apostrophe
Version:
The Apostrophe Content Management System.
695 lines (594 loc) • 20 kB
JavaScript
const assert = require('assert');
describe('moog', function() {
it('should exist', function() {
const moog = require('../lib/moog.js')({ });
assert(moog);
});
it('should be initialized without arguments', function() {
const moog = require('../lib/moog.js')();
assert(moog);
});
describe('methods', function() {
it('should have a `define` method', function() {
const moog = require('../lib/moog.js')({});
assert(moog.define);
});
it('should have a `redefine` method', function() {
const moog = require('../lib/moog.js')({});
assert(moog.redefine);
});
it('should have a `create` method', function() {
const moog = require('../lib/moog.js')({});
assert(moog.create);
});
it('should have an `isDefined` method', function() {
const moog = require('../lib/moog.js')({});
assert(moog.isDefined);
});
});
describe('defining and creating', function() {
it('should be able to `define` a class', async function() {
const moog = require('../lib/moog.js')({});
await moog.define('myObject', {
methods(self) {
return {};
}
});
});
it('should be able to `define` and then `create` an instance', async function() {
const moog = require('../lib/moog.js')({});
await moog.define('myObject', {
options: {
color: 'blue'
},
init(self) {
self.color = self.options.color;
}
});
const myObject = await moog.create('myObject', {});
assert(myObject);
assert(myObject.color === 'blue');
});
});
describe('`create` syntax', function() {
it('should `create` without options', async function() {
const moog = require('../lib/moog.js')();
await moog.define('myClass', {});
const myObj = await moog.create('myClass');
assert(myObj);
assert(myObj.__meta.name === 'myClass');
});
});
describe('explicit subclassing behavior', function() {
it('should be able to create a subclass with expected default option behavior (async)', async function() {
const moog = require('../lib/moog.js')({});
await moog.define('baseClass', {
options: {
color: 'blue'
}
});
await moog.define('subClass', {
options: {
color: 'red'
},
extend: 'baseClass'
});
const myObject = await moog.create('subClass', {});
assert(myObject);
assert(myObject.options.color === 'red');
});
it('should report an error gracefully if subclass to be extended does not exist (async)', async function() {
const moog = require('../lib/moog.js')({});
// base class does not actually exist
await moog.define('subClass', {
options: {
color: 'red'
},
extend: 'baseClass'
});
try {
await moog.create('subClass', {});
assert(false);
} catch (e) {
assert(e);
assert(e.toString().match(/baseClass/));
}
});
it('should be able to `extend` a subclass into yet another subclass', async function() {
const moog = require('../lib/moog.js')({});
await moog.define('baseClass', {
options: {
color: 'blue'
}
});
await moog.define('subClassOne', {
options: {
color: 'red'
},
extend: 'baseClass'
});
await moog.define('subClassTwo', {
options: {
color: 'green'
},
extend: 'subClassOne'
});
const myObject = await moog.create('subClassTwo', {});
assert(myObject);
assert(myObject.options.color === 'green');
});
it('default base class should take effect if configured', async function() {
const moog = require('../lib/moog.js')({ defaultBaseClass: 'baseClass' });
await moog.define('baseClass', {
init(self) {
assert(self.__meta);
assert(self.__meta.chain);
assert(self.__meta.chain[0]);
assert(self.__meta.chain[0].name === 'baseClass');
assert(self.__meta.chain[1].name === 'subClass');
assert(self.__meta.name === 'subClass');
self.color = self.options.color;
}
});
await moog.define('subClass', {
options: {
color: 'red'
}
});
const myObject = await moog.create('subClass', {});
assert(myObject);
// This verifies that init() for base class actually ran
assert(myObject.color === 'red');
});
it('default base class should not take effect if extend is explicitly set to false', async function() {
const moog = require('../lib/moog.js')({ defaultBaseClass: 'baseClass' });
await moog.define('baseClass', {
construct: function(self) {
self.based = true;
}
});
await moog.define('subClass', {
options: {
color: 'red'
},
extend: false
});
const myObject = await moog.create('subClass', {});
assert(myObject);
assert(!myObject.based);
});
it('should define methods and allow overriding and extending them through implicit subclassing', async function() {
const moog = require('../lib/moog.js')({});
await moog.define('myObject', {
methods(self) {
return {
basic() {
return true;
},
overridden() {
return false;
},
extended(times) {
return 2 * times;
}
};
}
});
await moog.define('myObject', {
methods(self) {
return {
overridden() {
return true;
}
};
},
extendMethods(self) {
return {
extended(_super, times) {
return _super(times) * 2;
}
};
}
});
const myObject = await moog.create('myObject', {});
assert(myObject);
assert(myObject.basic());
assert(myObject.overridden());
assert(myObject.extended(5) === 20);
});
it('should support inheriting field group fields rather than requiring all fields to be restated', async function() {
const moog = require('../lib/moog.js')({});
await moog.define('myObject', {
cascades: [ 'fields' ],
fields: {
add: {
one: { type: 'string' },
two: { type: 'string' },
three: { type: 'string' }
},
group: {
basics: {
fields: [ 'one', 'two', 'three' ]
}
}
}
});
await moog.define('myObject', {
fields: {
add: {
four: { type: 'string' },
five: { type: 'string' }
},
group: {
basics: {
fields: [ 'four', 'five' ]
},
other: {
fields: [ 'one' ]
}
}
}
});
const myObject = await moog.create('myObject', {});
assert(myObject);
assert(myObject.fieldsGroups);
assert(!myObject.fieldsGroups.basics.fields.includes('one'));
assert(myObject.fieldsGroups.other.fields.includes('one'));
assert(myObject.fieldsGroups.basics.fields.includes('two'));
assert(myObject.fieldsGroups.basics.fields.includes('three'));
assert(myObject.fieldsGroups.basics.fields.includes('four'));
assert(myObject.fieldsGroups.basics.fields.includes('five'));
});
it('should support operations alias of group fields', async function() {
const moog = require('../lib/moog.js')({});
await moog.define('myObject', {
cascades: [ 'batchOperations' ],
batchOperations: {
add: {
one: { label: 'One' },
two: { label: 'Two' },
three: { label: 'Three' }
},
group: {
more: {
operations: [ 'one', 'two', 'three' ]
}
}
}
});
await moog.define('myObject', {
batchOperations: {
add: {
four: { label: 'Four' },
five: { label: 'Five' }
},
group: {
more: {
operations: [ 'four', 'five' ]
},
other: {
operations: [ 'one' ]
}
}
}
});
const myObject = await moog.create('myObject', {});
assert(myObject);
assert(myObject.batchOperationsGroups);
assert(!myObject.batchOperationsGroups.more.operations.includes('one'));
assert(myObject.batchOperationsGroups.other.operations.includes('one'));
assert(myObject.batchOperationsGroups.more.operations.includes('two'));
assert(myObject.batchOperationsGroups.more.operations.includes('three'));
assert(myObject.batchOperationsGroups.more.operations.includes('four'));
assert(myObject.batchOperationsGroups.more.operations.includes('five'));
});
it('should order fields with the last option unless the order array overrides', async function() {
const moog = require('../lib/moog.js')({});
await moog.define('unorderedObject', {
cascades: [ 'fields' ],
fields: {
add: {
first: { type: 'string' },
last: {
type: 'string',
last: true
},
second: { type: 'string' },
third: { type: 'string' }
}
}
});
await moog.define('orderedObject', {
cascades: [ 'fields' ],
fields: {
add: {
first: { type: 'string' },
last: {
type: 'string',
last: true
},
second: { type: 'string' },
third: { type: 'string' }
},
order: [ 'last', 'third', 'second', 'first' ]
}
});
const unordered = await moog.create('unorderedObject', {});
assert(unordered);
assert(Object.keys(unordered.fields)[0] === 'first');
assert(Object.keys(unordered.fields)[1] === 'second');
assert(Object.keys(unordered.fields)[2] === 'third');
assert(Object.keys(unordered.fields)[3] === 'last');
const ordered = await moog.create('orderedObject', {});
assert(ordered);
assert(Object.keys(ordered.fields)[0] === 'last');
assert(Object.keys(ordered.fields)[1] === 'third');
assert(Object.keys(ordered.fields)[2] === 'second');
assert(Object.keys(ordered.fields)[3] === 'first');
});
// ==================================================
// `redefine` AND `isDefined`
// ==================================================
it('should allow a module to be redefined', async function() {
const moog = require('../lib/moog.js')({});
await moog.define('myObject', {
methods(self) {
return {
oldMethod() {}
};
}
});
await moog.redefine('myObject', {
methods(self) {
return {
newMethod() {}
};
}
});
const myObject = await moog.create('myObject', {});
assert(myObject);
assert(!myObject.oldMethod);
assert(myObject.newMethod);
});
it('should find a module definition using `isDefined`', async function() {
const moog = require('../lib/moog.js')({});
await moog.define('myObject', {});
assert.equal(await moog.isDefined('myObject'), true);
});
it('should NOT find a non-existant module definition using `isDefined`', async function() {
const moog = require('../lib/moog.js')({});
assert.equal(await moog.isDefined('myObject'), false);
});
});
describe('implicit subclassing behavior', function() {
it('should allow a class defined twice to be implicitly subclassed', async function() {
const moog = require('../lib/moog.js')({});
await moog.define('myObject', {
init(self) {
self.order = (self.order || []).concat('first');
}
});
await moog.define('myObject', {
init(self) {
self.order = (self.order || []).concat('second');
}
});
const myObject = await moog.create('myObject', {});
assert(myObject);
assert(myObject.order[0] === 'first');
assert(myObject.order[1] === 'second');
});
});
describe('order of operations', function() {
// ==================================================
// ORDERING
// ==================================================
it('should call `init` methods baseClass-first', async function() {
const moog = require('../lib/moog.js')({});
await moog.define('baseClass', {
init(self) {
self.order = (self.order || []).concat('first');
}
});
await moog.define('subClassOne', {
extend: 'baseClass',
init(self) {
self.order = (self.order || []).concat('second');
}
});
await moog.define('subClassTwo', {
extend: 'subClassOne',
init(self) {
self.order = (self.order || []).concat('third');
}
});
const subClassTwo = await moog.create('subClassTwo', {});
assert(subClassTwo.order[0] === 'first');
assert(subClassTwo.order[1] === 'second');
assert(subClassTwo.order[2] === 'third');
});
it('should call `beforeSuperClass` methods subClass-first', async function() {
const moog = require('../lib/moog.js')({});
await moog.define('baseClass', {
beforeSuperClass(self) {
self.options.order = (self.options.order || []).concat('third');
}
});
await moog.define('subClassOne', {
extend: 'baseClass',
beforeSuperClass(self) {
self.options.order = (self.options.order || []).concat('second');
}
});
await moog.define('subClassTwo', {
extend: 'subClassOne',
beforeSuperClass(self) {
self.options.order = (self.options.order || []).concat('first');
}
});
const subClassTwo = await moog.create('subClassTwo', {});
assert(subClassTwo.options.order[0] === 'first');
assert(subClassTwo.options.order[1] === 'second');
assert(subClassTwo.options.order[2] === 'third');
});
// "sync and async playing nicely" and exception-catching
// tests eliminated because the built-in language functionality
// of async/await now handles those jobs and is independently tested. -Tom
});
describe('odds and ends', function() {
it('should report an error on a cyclical reference (extend in a loop)', async function() {
const moog = require('../lib/moog.js')({});
await moog.define('classOne', {
extend: 'classTwo'
});
await moog.define('classTwo', {
extend: 'classOne'
});
let e;
let classOne;
try {
classOne = await moog.create('classOne', {});
} catch (_e) {
e = _e;
}
assert(e);
assert(!classOne);
});
it('should report an error asynchronously when creating a nonexistent type asynchronously', async function() {
const moog = require('../lib/moog.js')({});
try {
await moog.create('nonesuch');
assert(false);
} catch (e) {
assert(true);
}
});
it('instanceOf should yield correct results', async function() {
const moog = require('../lib/moog.js')({});
await moog.define('classOne', {});
await moog.define('classTwo', {
extend: 'classOne'
});
await moog.define('classThree', {});
await moog.define('classFour', {
extend: 'classTwo'
});
const one = await moog.create('classOne');
const two = await moog.create('classTwo');
const three = await moog.create('classThree');
const four = await moog.create('classFour');
const rando = { strange: 'object' };
assert(moog.instanceOf(one, 'classOne'));
assert(moog.instanceOf(two, 'classOne'));
assert(!moog.instanceOf(three, 'classOne'));
assert(moog.instanceOf(four, 'classOne'));
assert(!moog.instanceOf(rando, 'classOne'));
});
it('sanity check of await behavior', async function() {
const moog = require('../lib/moog.js')({});
await moog.define('classOne', {
async init(self) {
await delay(100);
self.size = 1;
}
});
await moog.define('classTwo', {
extend: 'classOne',
async init(self) {
await delay(1);
self.size = 2;
}
});
assert((await moog.create('classTwo', {})).size === 2);
});
it('isMy behaves sensibly', function() {
const moog = require('../lib/moog.js')({});
assert(moog.isMy('my-foo'));
assert(!moog.isMy('foo'));
assert(moog.isMy('@namespace/my-foo'));
assert(!moog.isMy('@namespace/foo'));
});
it('originalToMy behaves sensibly', function() {
const moog = require('../lib/moog.js')({});
assert(moog.originalToMy('foo') === 'my-foo');
assert(moog.originalToMy('@namespace/foo') === '@namespace/my-foo');
// originalToMy is not guaranteed to do anything specific with
// names that already have my-
});
it('myToOriginal behaves sensibly', function() {
const moog = require('../lib/moog.js')({});
assert(moog.myToOriginal('my-foo') === 'foo');
assert(moog.myToOriginal('foo') === 'foo');
assert(moog.myToOriginal('@namespace/my-foo') === '@namespace/foo');
assert(moog.myToOriginal('@namespace/foo') === '@namespace/foo');
});
});
describe('sections should work', function() {
it('routes should work, including extending routes', async function() {
const moog = require('../lib/moog.js')({ sections: [ 'routes' ] });
await moog.define('baseclass', {
routes(self) {
return {
post: {
async insert(req) {
return 'inserted';
},
async list(req) {
return 'listed';
}
}
};
}
});
await moog.define('subclass', {
extend: 'baseclass',
routes(self) {
return {
post: {
async remove(req) {
return 'removed';
},
floss: [
(req, res, next) => {
req.seen = true;
next();
},
(req) => 'flossed'
]
}
};
},
extendRoutes(self) {
return {
post: {
async list(_super, req) {
return (await _super(req)) + '-suffix';
},
async floss(_super, req) {
return (await _super(req)) + '-daily';
}
}
};
}
});
const instance = await moog.create('subclass', {});
assert(instance);
assert((await instance.routes.post.insert({})) === 'inserted');
assert((await instance.routes.post.list({})) === 'listed-suffix');
assert((await instance.routes.post.remove({})) === 'removed');
const req = {};
const floss = await instance.routes.post.floss;
// We are not here to reimplement middleware, just to verify
// the chain is there and would work
assert((typeof floss[0]) === 'function');
floss[0](req, {}, function() {});
assert(req.seen);
assert((await floss[1](req)) === 'flossed-daily');
});
});
});
function delay(ms) {
return new Promise(function(resolve, reject) {
setTimeout(() => resolve(true), ms);
});
}