angular-model
Version:
Simple HATEOS-oriented persistence module for AngularJS.
817 lines (664 loc) • 24.9 kB
JavaScript
describe('model', function() {
var provider;
beforeEach(module('ur.model', function(modelProvider) {
provider = modelProvider;
}));
beforeEach(function() {
jasmine.addMatchers({
toEqualData: function() {
return {
compare: function(actual, expected) {
return {pass: angular.equals(actual, expected)};
}
};
}
});
});
describe('provider', function() {
describe('configuration', function() {
it('should accept definitions', inject(function() {
expect(provider.model('Projects', {})).toBe(provider);
}));
it('should not wrongly overwrite default configuration', inject(function(model) {
provider.model('Lists', {
url: 'http://api/lists'
}).model('Items', {
url: 'http://api/items'
});
expect(model('Lists').url()).toBe('http://api/lists');
expect(model('Items').url()).toBe('http://api/items');
}));
});
it('should be undefined for undefined models', inject(function(model) {
expect(model('Foo')).toBeUndefined();
}));
it('should auto-generate a URL for new models', inject(function(model) {
provider.model('ListItems', {});
expect(model('ListItems').url()).toBe('/list-items');
}));
it('shouldn\'t allow configuration properties to be overwritten', inject(function(model) {
provider.model('ListItems', {url: '/foo'});
model('ListItems').$config().url = '/bar';
expect(model('ListItems').url()).toBe('/foo');
}));
});
describe('service', function() {
var $httpBackend;
beforeEach(module(function() {
provider.model('Users', {
defaults: {username: 'anon'},
url: 'http://api/users',
$instance: {
isAdmin: function() {
return this.roles.indexOf('admin') > -1;
}
}
}).model('Projects', {
$instance: {
poster: function() {
return this.$links.logo || '/img/placeholder/poster.gif';
},
fileName: function() {
var i = this.name.lastIndexOf('_');
return (i !== -1) ? this.name.substring(i + 1) : this.name;
}
},
defaults: {
name: 'New Project',
$links: {}
},
url: 'http://api/projects'
}).model('Tasks', {
url: 'http://api/tasks'
}).model('Messages', {
url: 'http://api/messages'
});
}));
beforeEach(inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('should allow late-binding of requests', inject(function(model) {
model('Sessions', {
url: 'http://api/sessions',
defaults: {id: 12345}
});
expect(model('Sessions').create().id).toBe(12345);
}));
it('should create objects with configured default values', inject(function(model) {
var newProject = model('Projects').create();
expect(newProject).toEqualData({name: 'New Project', $links: {}});
var newUser = model('Users').create();
expect(newUser).toEqualData({username: 'anon'});
}));
it('should invoke instance methods with the correct binding', inject(function(model) {
var Projects = model('Projects'), customVal = '/img/my-project.png';
expect(Projects.create().poster()).toEqual('/img/placeholder/poster.gif');
expect(Projects.create({$links: {logo: customVal}}).poster()).toEqual(customVal);
}));
it('should determine if a relation exists', inject(function(model) {
var project = model('Projects').create({
$links: {
self: {href: 'http://api/projects/10'},
owner: {href: 'http://api/users/1', name: 'Users'}
}
});
expect(project.$hasRelated('owner')).toBe(true);
expect(project.$hasRelated('watcher')).toBe(false);
}));
it('should load related instances by name', inject(function(model) {
var project = model('Projects').create({
$links: {
self: {href: 'http://api/projects/10'},
owner: {href: 'http://api/users/1', name: 'Users'}
}
});
project.$related('owner');
$httpBackend.expectGET('http://api/users/1').respond({
$links: {self: {href: 'http://api/users/1'}}
});
$httpBackend.flush();
}));
it('should throw an error if relation does not exist', inject(function(model) {
var project = model('Projects').create({
$links: {
self: {href: 'http://api/projects/10'},
owner: {href: 'http://api/users/1', name: 'Users'}
}
});
var fn = function() {
project.$related('foo');
};
expect(fn).toThrow(new Error('Relation `foo` does not exist.'));
$httpBackend.verifyNoOutstandingRequest();
}));
it('should box relations according to name', inject(function(model) {
var project = model('Projects').create({
$links: {
self: {href: 'http://api/projects/10'},
owner: {href: 'http://api/users/1', name: 'Users'}
}
});
var owner = project.$related('owner').then(function(owner) {
expect(owner.isAdmin).toEqual(jasmine.any(Function));
});
$httpBackend.expectGET('http://api/users/1').respond({
$links: {self: {href: 'http://api/users/1', name: 'Users'}}
});
$httpBackend.flush();
}));
it('should create new objects with a POST request', inject(function(model) {
$httpBackend.expectPOST('http://api/projects', JSON.stringify({
name: 'My Project',
$links: {},
verified: true
})).respond(201, JSON.stringify({
name: 'My Project',
archived: false,
$links: {self: {href: 'http://api/projects/1138', name: 'Projects'}}
}));
var newProject = model('Projects').create({name: 'My Project'});
newProject.$save({verified: true}).then(function(result) {
expect(result).toBe(newProject);
});
expect(newProject.$links.self).toBeUndefined();
expect(newProject.archived).toBeUndefined();
$httpBackend.flush();
expect(newProject.$links.self.href).toBe('http://api/projects/1138');
expect(newProject.archived).toBe(false);
expect(newProject.verified).toBe(true);
}));
it('should update existing objects with a PATCH request', inject(function(model) {
var url = 'http://api/projects/1138', doc = {
name: 'My Project',
verified: true,
archived: false
}, update = angular.extend({}, doc, {archived: true});
$httpBackend.expectPATCH(url, JSON.stringify({archived: true})).respond(200, JSON.stringify(update));
var existingProject = model('Projects').instance(angular.extend({
$links: {self: {href: url,}}
}, doc));
existingProject.$save({archived: true}).then(function(result) {
expect(result).toBe(existingProject);
});
$httpBackend.flush();
expect(existingProject.name).toBe('My Project');
expect(existingProject.$links.self.href).toEqual('http://api/projects/1138');
expect(existingProject.archived).toBe(true);
}));
it('should return arrays boxed as collections', inject(function(model) {
var data = [
{name: 'First Project', $links: {self: {href: 'http://api/projects/1138'}}},
{name: 'Second Project', $links: {self: {href: 'http://api/projects/1139'}}}
];
$httpBackend.expectGET('http://api/projects').respond(200, JSON.stringify(data));
model('Projects').all().then(function(result) {
expect(angular.isArray(result)).toBe(true);
expect(result.length).toBe(2);
expect(result.$model().url()).toBe('http://api/projects');
});
$httpBackend.flush();
}));
it('should attach and correctly bind custom collection methods', inject(function(model) {
provider.model('Objects', {
$collection: {
count: function() {
return this.length;
}
}
});
$httpBackend.expectGET('/objects').respond(200, JSON.stringify([{}, {}, {}, {}]));
model('Objects').all().then(function(collection) {
expect(collection.count()).toBe(4);
});
$httpBackend.flush();
}));
it('should accept query parameters', inject(function(model) {
var success, data = {success: true};
$httpBackend.expectGET('http://api/projects?q=some%20search').respond(200, JSON.stringify(data));
model('Projects').all({q: 'some search'}).then(function(result) {
success = result;
});
$httpBackend.flush();
expect(success.success).toBe(true);
}));
it('should correctly append query parameters', inject(function(model) {
var success, data = {success: true};
provider.model('Search', {url: 'http://api/search?hl=en'});
$httpBackend.expectGET('http://api/search?hl=en&q=some%20search').respond(200, JSON.stringify(data));
model('Search').all({q: 'some search'}).then(function(result) {
success = result;
});
$httpBackend.flush();
expect(success.success).toBe(true);
}));
it('should accept custom headers', inject(function(model) {
var success, data = {success: true};
$httpBackend.expectGET('http://api/projects', {
'Accept': 'application/json, text/plain, */*',
'x-some-header': 'foo'
}).respond(200, JSON.stringify(data));
model('Projects').all(null, {
'x-some-header': 'foo'
}).then(function(result) {
success = result;
});
$httpBackend.flush();
expect(success.success).toBe(true);
}));
describe('load()', function() {
it('should map promise resolution values to object attributes asynchronously', inject(function(model) {
var scope = {},
projects = [{name: 'Project 1'}, {name: 'Project 2'}],
tasks = [{name: 'Task 1'}, {name: 'Task 2'}];
$httpBackend.expectGET('http://api/projects').respond(200, JSON.stringify(projects));
$httpBackend.expectGET('http://api/tasks').respond(200, JSON.stringify(tasks));
model.load(scope, {
projects: model('Projects').all(),
tasks: model('Tasks').all()
}).then(function() {
scope.done = true;
});
expect(scope.projects).toBeUndefined();
expect(scope.tasks).toBeUndefined();
expect(scope.done).toBeUndefined();
$httpBackend.flush(1);
expect(JSON.stringify(scope.projects)).toBe(JSON.stringify(projects));
expect(scope.tasks).toBeUndefined();
expect(scope.done).toBeUndefined();
$httpBackend.flush();
expect(JSON.stringify(scope.tasks)).toBe(JSON.stringify(tasks));
expect(scope.done).toBe(true);
}));
});
describe('class methods', function() {
describe('first()', function() {
it('should return the first element of an array', inject(function(model) {
var data = [
{name: 'First Project', $links: {self: {href: 'http://api/projects/1138'}}},
{name: 'Second Project', $links: {self: {href: 'http://api/projects/1139'}}}
];
$httpBackend.expectGET('http://api/projects').respond(200, JSON.stringify(data));
model('Projects').first().then(function(result) {
expect(result.name).toBe('First Project');
});
$httpBackend.flush();
}));
it('should return the full result of non-array responses', inject(function(model) {
var data = {name: 'A Project', $links: {self: {href: 'http://api/projects/a'}}};
$httpBackend.expectGET('http://api/projects').respond(200, JSON.stringify(data));
model('Projects').first().then(function(result) {
expect(result.name).toBe('A Project');
expect(result.$links.self.href).toBe('http://api/projects/a');
});
$httpBackend.flush();
}));
it('should passthru query parameters', inject(function(model) {
var first, data = {name: 'First Project', $links: {self: {href: 'http://api/projects/first'}}};
$httpBackend.expectGET('http://api/projects?first=true').respond(200, JSON.stringify(data));
model('Projects').first({first: true}).then(function(result) {
first = result;
});
$httpBackend.flush();
expect(first.name).toBe('First Project');
expect(first.$links.self.href).toBe('http://api/projects/first');
}));
});
});
describe('errors', function() {
it('should populate on failed request', inject(function(model) {
var user, response, id = 'http://api/users/5', errors = {
email: [
'E-mail cannot be empty.',
'E-mail is not valid.',
'Sorry, this e-mail address is already registered.'
],
passwordConfirm: 'Your passwords must match.'
};
$httpBackend.expectPATCH(id).respond(422, JSON.stringify(errors));
user = model('Users').create({$links: {self: {href: id}}});
user.$save({name: 'test'}).then(angular.noop, function(resp) {
response = resp;
});
expect(user.$errors).toBeUndefined();
$httpBackend.flush();
expect(response.status).toBe(422);
expect(JSON.stringify(user.$errors)).toEqual(JSON.stringify(errors));
}));
it('should handle a failed request to GET all', inject(function(model) {
$httpBackend.expectGET('http://api/users').respond(404, []);
users = model('Users').all();
$httpBackend.flush();
}));
});
describe('dirty checking', function() {
var user;
beforeEach(inject(function(model) {
user = model('Users').instance({
$links: {self: {href: 'http://api/users/100'}},
_id: 100,
name: 'Some Person',
role: 'User',
telephone: {
home: '1234',
office: '5678'
}
});
}));
it('should tell if a value has changed', function() {
expect(user.$dirty()).toBe(false);
expect(user.$pristine()).toBe(true);
user.name = 'Some other person';
expect(user.$dirty()).toBe(true);
expect(user.$pristine()).toBe(false);
});
it('should tell if a value is added', function() {
expect(user.$dirty()).toBe(false);
expect(user.$pristine()).toBe(true);
user.email = 'test@test.com';
expect(user.$dirty()).toBe(true);
expect(user.$pristine()).toBe(false);
});
it('should tell if a nested object has changed', function() {
expect(user.$dirty()).toBe(false);
expect(user.$pristine()).toBe(true);
user.telephone.office = '91011';
expect(user.$dirty()).toBe(true);
expect(user.$pristine()).toBe(false);
});
it('should revert to original state', function() {
user.name = 'Some other person';
user.telephone.office = '91011';
user.$revert();
expect(user.$dirty()).toBe(false);
var reverted = angular.extend({}, user, {
_id: 100,
name: 'Some Person',
role: 'User',
telephone: {
home: '1234',
office: '5678'
}
});
expect(angular.equals(reverted, user)).toBe(true);
expect(user.$original().telephone).not.toBe(user.telephone);
});
it('should return new and modified fields', function() {
angular.extend(user, {
name: 'New name',
role: 'Admin',
telephone: {
office: '91011'
},
email: 'test@test.com'
});
expect(user.$modified()).toEqual({
name: 'New name',
role: 'Admin',
telephone: {
office: '91011'
},
email: 'test@test.com'
});
});
it('should only PATCH new and modified fields', function() {
angular.extend(user, {
name: 'Newman',
role: 'Intern',
telephone: {
home: '0123'
},
email: 'test@test.com'
});
user.$save();
$httpBackend.expectPATCH('http://api/users/100', {
name: 'Newman',
role: 'Intern',
telephone: {
home: '0123'
},
email: 'test@test.com'
}).respond(200);
$httpBackend.flush();
});
it('should update original state on successful save', function() {
angular.extend(user, {
name: 'Newman',
role: 'Intern',
telephone: {
home: '0123'
},
newObj: {
foo: 'bar'
}
});
user.$save();
$httpBackend.expectPATCH('http://api/users/100', {
name: 'Newman',
role: 'Intern',
telephone: {
home: '0123'
},
newObj: {
foo: 'bar'
}
}).respond(200, {
$links: {
self: {
href: 'http://api/users/100'
}
},
_id: 100,
name: 'Newman',
role: 'Intern',
telephone: {
home: '0123',
office: '5678'
},
newObj: {
foo: 'bar'
}
});
$httpBackend.flush();
expect(user.$pristine()).toBe(true);
expect(user.$dirty()).toBe(false);
expect(user.$modified()).toEqual({});
expect(user.$original().newObj).not.toBe(user.newObj);
expect(user.$original().telephone).not.toBe(user.telephone);
});
it('should preserve modified state on failed save', function() {
angular.extend(user, {
name: 'Newman',
role: 'Intern',
telephone: {
home: '0123'
}
});
user.$save();
$httpBackend.expectPATCH('http://api/users/100', {
name: 'Newman',
role: 'Intern',
telephone: {
home: '0123'
}
}).respond(500);
$httpBackend.flush();
expect(user.$pristine()).toBe(false);
expect(user.$dirty()).toBe(true);
expect(user.$modified()).toEqual({
name: 'Newman',
role: 'Intern',
telephone: {
home: '0123'
}
});
});
it('should skip PATCH for an umodified instance and resolve immediately', function() {
user.$save().then(function(resp) {
expect(resp).toEqualData(user);
});
$httpBackend.verifyNoOutstandingRequest();
});
it('should ignore dirty fields when saving a new instance', inject(function(model) {
var admin = model('Users').create({
username: 'Mr Admin',
role: 'Admin'
});
admin.$save();
$httpBackend.expectPOST('http://api/users', {
username: 'Mr Admin',
role: 'Admin'
}).respond(100);
$httpBackend.flush();
}));
it('should push updates to arrays', inject(function(model) {
var list = model('Tasks').create({things: []});
list.$save();
$httpBackend.expectPOST('http://api/tasks', {
things: []
}).respond(201, {
things: [],
$links: {self: {href: 'http://api/tasks/1138'}}
});
$httpBackend.flush();
list.things = ['foo', 'bar'];
list.$save();
$httpBackend.expectPATCH('http://api/tasks/1138', {
things: ['foo', 'bar']
}).respond(200, {
things: ['foo', 'bar']
});
$httpBackend.flush();
expect(list.things).toEqualData(['foo', 'bar']);
expect(list.$pristine()).toBe(true);
list.things.pop();
expect(list.$pristine()).toBe(false);
list.$save();
$httpBackend.expectPATCH('http://api/tasks/1138', {
things: ['foo']
}).respond(200, {
things: ['foo']
});
$httpBackend.flush();
expect(list.things).toEqualData(['foo']);
expect(list.$pristine()).toBe(true);
}));
it('should not choke on null values in arrays', inject(function(model) {
var data = {foo: 'bar'};
model('Tasks').create(data).$save();
$httpBackend.expectPOST('http://api/tasks', data).respond(200, angular.extend(
{baz: ['gooby', null]}, data
));
$httpBackend.flush();
}));
});
describe('$delete()', function() {
it('erases the object form the API', inject(function(model) {
var selfurl = 'http://api/users/5';
var user = model('Users').create({$links: {self: {href: selfurl}}});
$httpBackend.expectDELETE(selfurl).respond(204);
user.$delete();
$httpBackend.flush();
}));
});
describe('syncing', function() {
it('should correctly update local copy on successful save', inject(function(model) {
var message = model('Messages').create({
from: 'http://api/users/13',
content: 'Hello!'
});
message.$save();
$httpBackend.expectPOST('http://api/messages', {
from: 'http://api/users/13',
content: 'Hello!'
}).respond(201, {
content: 'Hello!',
from: {
name: 'Bob',
$links: {self: {href: 'http://api/users/13'}}
}
});
$httpBackend.flush();
expect(message.from).toEqualData({
name: 'Bob',
$links: {self: {href: 'http://api/users/13'}}
});
}));
});
describe('$collection', function() {
beforeEach(function() {
var data = [
{name: 'First Project', $links: {self: {href: 'http://api/projects/1138'}}},
{name: 'Second Project', $links: {self: {href: 'http://api/projects/1139'}}}
];
$httpBackend.expectGET('http://api/projects').respond(200, JSON.stringify(data));
});
describe('add()', function() {
it('adds a new model instance to the API', inject(function(model) {
$httpBackend.expectPOST('http://api/projects/1140').respond(201);
model('Projects').all().then(function(result) {
var data = {name: 'Third Project', $links: {self: {href: 'http://api/projects/1140'}}};
var newItem = model('Projects').create({});
result.add(newItem, data);
});
$httpBackend.flush();
}));
});
describe('remove()', function() {
it('removes items from the API and from the cache', inject(function(model) {
$httpBackend.expectDELETE('http://api/projects/1138').respond(204);
model('Projects').all().then(function(result) {
expect(result.length).toEqual(2);
result.remove(0).then(function() {
expect(result.length).toEqual(1);
});
});
$httpBackend.flush();
}));
});
});
});
describe('directive', function() {
var $httpBackend;
beforeEach(inject(function($injector) {
$httpBackend = $injector.get('$httpBackend');
}));
afterEach(function() {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
describe('link directive', function() {
var elm, scope;
describe('when a relation, resource and href are provided', function() {
beforeEach(inject(function($rootScope, $compile) {
elm = angular.element('<link rel=\'resource\' name=\'Messages\' href=\'/api-of-your-system/Messages\'>');
scope = $rootScope;
$compile(elm)(scope);
scope.$digest();
}));
it('assigns the model\'s URL from the href attribute', inject(function(model) {
var message = model('Messages').create({
content: 'Hello!'
});
message.$save();
$httpBackend.expectPOST('/api-of-your-system/Messages').respond(201);
$httpBackend.flush();
}));
});
describe('when rel !== resource', function() {
beforeEach(inject(function($rootScope, $compile) {
elm = angular.element('<link rel=\'other\' name=\'Messages\' href=\'/api-of-your-system/Messages\'>');
scope = $rootScope;
$compile(elm)(scope);
scope.$digest();
}));
it('is ignored', inject(function(model) {
expect(model('Messages')).toBeUndefined();
}));
});
});
});
});