Commit 682088d7 authored by Sean Hammond's avatar Sean Hammond Committed by GitHub

Merge pull request #4 from hypothesis/remove-ngresource-for-api-calls

Remove use of ngResource for search and annotation queries
parents 8a5ab37d 0d9217ec
...@@ -24,7 +24,7 @@ function annotationMapper($rootScope, annotationUI, store) { ...@@ -24,7 +24,7 @@ function annotationMapper($rootScope, annotationUI, store) {
$rootScope.$emit(events.ANNOTATION_UPDATED, existing); $rootScope.$emit(events.ANNOTATION_UPDATED, existing);
return; return;
} }
loaded.push(new store.AnnotationResource(annotation)); loaded.push(annotation);
}); });
$rootScope.$emit(events.ANNOTATIONS_LOADED, loaded); $rootScope.$emit(events.ANNOTATIONS_LOADED, loaded);
...@@ -42,13 +42,12 @@ function annotationMapper($rootScope, annotationUI, store) { ...@@ -42,13 +42,12 @@ function annotationMapper($rootScope, annotationUI, store) {
} }
function createAnnotation(annotation) { function createAnnotation(annotation) {
annotation = new store.AnnotationResource(annotation);
$rootScope.$emit(events.BEFORE_ANNOTATION_CREATED, annotation); $rootScope.$emit(events.BEFORE_ANNOTATION_CREATED, annotation);
return annotation; return annotation;
} }
function deleteAnnotation(annotation) { function deleteAnnotation(annotation) {
return annotation.$delete({ return store.annotation.delete({
id: annotation.id id: annotation.id
}).then(function () { }).then(function () {
$rootScope.$emit(events.ANNOTATION_DELETED, annotation); $rootScope.$emit(events.ANNOTATION_DELETED, annotation);
......
...@@ -9,16 +9,16 @@ var angular = require('angular'); ...@@ -9,16 +9,16 @@ var angular = require('angular');
*/ */
function fetchThread(store, id) { function fetchThread(store, id) {
var annot; var annot;
return store.AnnotationResource.get({id: id}).$promise.then(function (annot) { return store.annotation.get({id: id}).then(function (annot) {
if (annot.references && annot.references.length) { if (annot.references && annot.references.length) {
// This is a reply, fetch the top-level annotation // This is a reply, fetch the top-level annotation
return store.AnnotationResource.get({id: annot.references[0]}).$promise; return store.annotation.get({id: annot.references[0]});
} else { } else {
return annot; return annot;
} }
}).then(function (annot_) { }).then(function (annot_) {
annot = annot_; annot = annot_;
return store.SearchResource.get({references: annot.id}).$promise; return store.search({references: annot.id});
}).then(function (searchResult) { }).then(function (searchResult) {
return [annot].concat(searchResult.rows); return [annot].concat(searchResult.rows);
}); });
......
...@@ -34,10 +34,6 @@ var resolve = { ...@@ -34,10 +34,6 @@ var resolve = {
sessionState: function (session) { sessionState: function (session) {
return session.load(); return session.load();
}, },
// @ngInject
store: function (store) {
return store.$promise;
},
streamer: streamer.connect, streamer: streamer.connect,
}; };
......
...@@ -26,7 +26,7 @@ module.exports = class CrossFrame ...@@ -26,7 +26,7 @@ module.exports = class CrossFrame
formatted[k] = v formatted[k] = v
formatted formatted
parser: (annotation) -> parser: (annotation) ->
parsed = new store.AnnotationResource() parsed = {}
for k, v of annotation when k in whitelist for k, v of annotation when k in whitelist
parsed[k] = v parsed[k] = v
parsed parsed
......
...@@ -124,12 +124,33 @@ function updateViewModel($scope, domainModel, ...@@ -124,12 +124,33 @@ function updateViewModel($scope, domainModel,
function AnnotationController( function AnnotationController(
$document, $q, $rootScope, $scope, $timeout, $window, annotationUI, $document, $q, $rootScope, $scope, $timeout, $window, annotationUI,
annotationMapper, drafts, flash, features, groups, permissions, session, annotationMapper, drafts, flash, features, groups, permissions, session,
settings) { settings, store) {
var vm = this; var vm = this;
var domainModel; var domainModel;
var newlyCreatedByHighlightButton; var newlyCreatedByHighlightButton;
/** Save an annotation to the server. */
function save(annot) {
var saved;
if (annot.id) {
saved = store.annotation.update({id: annot.id}, annot);
} else {
saved = store.annotation.create({}, annot);
}
return saved.then(function (savedAnnot) {
// Copy across internal properties which are not part of the annotation
// model saved on the server
savedAnnot.$$tag = annot.$$tag;
Object.keys(annot).forEach(function (k) {
if (k[0] === '$') {
savedAnnot[k] = annot[k];
}
});
return savedAnnot;
});
}
/** /**
* Initialize this AnnotationController instance. * Initialize this AnnotationController instance.
* *
...@@ -301,8 +322,9 @@ function AnnotationController( ...@@ -301,8 +322,9 @@ function AnnotationController(
// User is logged in, save to server. // User is logged in, save to server.
// Highlights are always private. // Highlights are always private.
domainModel.permissions = permissions.private(); domainModel.permissions = permissions.private();
domainModel.$create().then(function() { save(domainModel).then(function(model) {
$rootScope.$emit(events.ANNOTATION_CREATED, domainModel); domainModel = model;
$rootScope.$emit(events.ANNOTATION_CREATED, model);
updateView(domainModel); updateView(domainModel);
}); });
} else { } else {
...@@ -513,31 +535,23 @@ function AnnotationController( ...@@ -513,31 +535,23 @@ function AnnotationController(
return Promise.resolve(); return Promise.resolve();
} }
var saved; var updatedModel = angular.copy(domainModel);
switch (vm.action) {
case 'create':
updateDomainModel(domainModel, vm, permissions);
saved = domainModel.$create().then(function () {
$rootScope.$emit(events.ANNOTATION_CREATED, domainModel);
updateView(domainModel);
drafts.remove(domainModel);
});
break;
case 'edit':
var updatedModel = angular.copy(domainModel);
updateDomainModel(updatedModel, vm, permissions);
saved = updatedModel.$update({
id: updatedModel.id
}).then(function () {
drafts.remove(domainModel);
$rootScope.$emit(events.ANNOTATION_UPDATED, updatedModel);
});
break;
default: // Copy across the non-enumerable local tag for the annotation
throw new Error('Tried to save an annotation that is not being edited'); updatedModel.$$tag = domainModel.$$tag;
}
updateDomainModel(updatedModel, vm, permissions);
var saved = save(updatedModel).then(function (model) {
var isNew = !domainModel.id;
drafts.remove(domainModel);
domainModel = model;
if (isNew) {
$rootScope.$emit(events.ANNOTATION_CREATED, domainModel);
} else {
$rootScope.$emit(events.ANNOTATION_UPDATED, domainModel);
}
updateView(domainModel);
});
// optimistically switch back to view mode and display the saving // optimistically switch back to view mode and display the saving
// indicator // indicator
......
...@@ -138,6 +138,7 @@ describe('annotation', function() { ...@@ -138,6 +138,7 @@ describe('annotation', function() {
var fakeGroups; var fakeGroups;
var fakePermissions; var fakePermissions;
var fakeSession; var fakeSession;
var fakeStore;
var sandbox; var sandbox;
function createDirective(annotation) { function createDirective(annotation) {
...@@ -231,6 +232,17 @@ describe('annotation', function() { ...@@ -231,6 +232,17 @@ describe('annotation', function() {
get: function() {} get: function() {}
}; };
fakeStore = {
annotation: {
create: sinon.spy(function (annot) {
return Promise.resolve(Object.assign({}, annot));
}),
update: sinon.spy(function (annot) {
return Promise.resolve(Object.assign({}, annot));
}),
},
};
$provide.value('annotationMapper', fakeAnnotationMapper); $provide.value('annotationMapper', fakeAnnotationMapper);
$provide.value('annotationUI', fakeAnnotationUI); $provide.value('annotationUI', fakeAnnotationUI);
$provide.value('drafts', fakeDrafts); $provide.value('drafts', fakeDrafts);
...@@ -239,6 +251,7 @@ describe('annotation', function() { ...@@ -239,6 +251,7 @@ describe('annotation', function() {
$provide.value('permissions', fakePermissions); $provide.value('permissions', fakePermissions);
$provide.value('session', fakeSession); $provide.value('session', fakeSession);
$provide.value('settings', fakeSettings); $provide.value('settings', fakeSettings);
$provide.value('store', fakeStore);
$provide.value('groups', fakeGroups); $provide.value('groups', fakeGroups);
})); }));
...@@ -327,60 +340,44 @@ describe('annotation', function() { ...@@ -327,60 +340,44 @@ describe('annotation', function() {
var annotation = fixtures.newHighlight(); var annotation = fixtures.newHighlight();
// The user is logged-in. // The user is logged-in.
annotation.user = fakeSession.state.userid = 'acct:bill@localhost'; annotation.user = fakeSession.state.userid = 'acct:bill@localhost';
annotation.$create = sandbox.stub().returns({
then: function() {}
});
createDirective(annotation); createDirective(annotation);
assert.called(annotation.$create); assert.called(fakeStore.annotation.create);
}); });
it('saves new highlights to drafts if not logged in', function() { it('saves new highlights to drafts if not logged in', function() {
var annotation = fixtures.newHighlight(); var annotation = fixtures.newHighlight();
// The user is not logged-in. // The user is not logged-in.
annotation.user = fakeSession.state.userid = undefined; annotation.user = fakeSession.state.userid = undefined;
annotation.$create = sandbox.stub().returns({
then: function() {}
});
createDirective(annotation); createDirective(annotation);
assert.notCalled(annotation.$create); assert.notCalled(fakeStore.annotation.create);
assert.called(fakeDrafts.update); assert.called(fakeDrafts.update);
}); });
it('does not save new annotations on initialization', function() { it('does not save new annotations on initialization', function() {
var annotation = fixtures.newAnnotation(); var annotation = fixtures.newAnnotation();
annotation.$create = sandbox.stub().returns({
then: function() {}
});
createDirective(annotation); createDirective(annotation);
assert.notCalled(annotation.$create); assert.notCalled(fakeStore.annotation.create);
}); });
it('does not save old highlights on initialization', function() { it('does not save old highlights on initialization', function() {
var annotation = fixtures.oldHighlight(); var annotation = fixtures.oldHighlight();
annotation.$create = sandbox.stub().returns({
then: function() {}
});
createDirective(annotation); createDirective(annotation);
assert.notCalled(annotation.$create); assert.notCalled(fakeStore.annotation.create);
}); });
it('does not save old annotations on initialization', function() { it('does not save old annotations on initialization', function() {
var annotation = fixtures.oldAnnotation(); var annotation = fixtures.oldAnnotation();
annotation.$create = sandbox.stub().returns({
then: function() {}
});
createDirective(annotation); createDirective(annotation);
assert.notCalled(annotation.$create); assert.notCalled(fakeStore.annotation.create);
}); });
it('edits new annotations on initialization', function() { it('edits new annotations on initialization', function() {
...@@ -403,10 +400,6 @@ describe('annotation', function() { ...@@ -403,10 +400,6 @@ describe('annotation', function() {
it('does not edit new highlights on initialization', function() { it('does not edit new highlights on initialization', function() {
var annotation = fixtures.newHighlight(); var annotation = fixtures.newHighlight();
// We have to set annotation.$create() because it'll try to call it.
annotation.$create = sandbox.stub().returns({
then: function() {}
});
var controller = createDirective(annotation).controller; var controller = createDirective(annotation).controller;
...@@ -432,27 +425,25 @@ describe('annotation', function() { ...@@ -432,27 +425,25 @@ describe('annotation', function() {
it('returns true if action is "create"', function() { it('returns true if action is "create"', function() {
var controller = createDirective().controller; var controller = createDirective().controller;
controller.action = 'create'; controller.action = 'create';
assert(controller.editing()); assert.equal(controller.editing(), true);
}); });
it('returns true if action is "edit"', function() { it('returns true if action is "edit"', function() {
var controller = createDirective().controller; var controller = createDirective().controller;
controller.action = 'edit'; controller.action = 'edit';
assert(controller.editing()); assert.equal(controller.editing(), true);
}); });
it('returns false if action is "view"', function() { it('returns false if action is "view"', function() {
var controller = createDirective().controller; var controller = createDirective().controller;
controller.action = 'view'; controller.action = 'view';
assert(!controller.editing()); assert.equal(controller.editing(), false);
}); });
}); });
describe('.isHighlight()', function() { describe('.isHighlight()', function() {
it('returns true for new highlights', function() { it('returns true for new highlights', function() {
var annotation = fixtures.newHighlight(); var annotation = fixtures.newHighlight();
// We need to define $create because it'll try to call it.
annotation.$create = function() {return {then: function() {}};};
var vm = createDirective(annotation).controller; var vm = createDirective(annotation).controller;
...@@ -520,11 +511,6 @@ describe('annotation', function() { ...@@ -520,11 +511,6 @@ describe('annotation', function() {
beforeEach(function() { beforeEach(function() {
annotation = fixtures.defaultAnnotation(); annotation = fixtures.defaultAnnotation();
annotation.$highlight = true; annotation.$highlight = true;
annotation.$create = sinon.stub().returns({
then: angular.noop,
'catch': angular.noop,
'finally': angular.noop
});
}); });
it('is private', function() { it('is private', function() {
...@@ -589,7 +575,7 @@ describe('annotation', function() { ...@@ -589,7 +575,7 @@ describe('annotation', function() {
}; };
}; };
controller.reply(); controller.reply();
assert(reply.permissions.read.indexOf('my group') !== -1); assert.notEqual(reply.permissions.read.indexOf('my group'), -1);
}); });
it( it(
...@@ -625,8 +611,6 @@ describe('annotation', function() { ...@@ -625,8 +611,6 @@ describe('annotation', function() {
parts.controller.isPrivate = false; parts.controller.isPrivate = false;
fakePermissions.isPrivate.returns(false); fakePermissions.isPrivate.returns(false);
parts.annotation.$update = sinon.stub().returns(Promise.resolve());
// Edit the annotation and make it private. // Edit the annotation and make it private.
parts.controller.edit(); parts.controller.edit();
parts.controller.setPrivacy('private'); parts.controller.setPrivacy('private');
...@@ -642,7 +626,6 @@ describe('annotation', function() { ...@@ -642,7 +626,6 @@ describe('annotation', function() {
it('makes the annotation shared when level is "shared"', function() { it('makes the annotation shared when level is "shared"', function() {
var parts = createDirective(); var parts = createDirective();
parts.controller.isPrivate = true; parts.controller.isPrivate = true;
parts.annotation.$update = sinon.stub().returns(Promise.resolve());
parts.controller.edit(); parts.controller.edit();
parts.controller.form.text = 'test'; parts.controller.form.text = 'test';
parts.controller.setPrivacy('shared'); parts.controller.setPrivacy('shared');
...@@ -653,33 +636,30 @@ describe('annotation', function() { ...@@ -653,33 +636,30 @@ describe('annotation', function() {
it('saves the "shared" visibility level to localStorage', function() { it('saves the "shared" visibility level to localStorage', function() {
var parts = createDirective(); var parts = createDirective();
parts.annotation.$update = sinon.stub().returns(Promise.resolve());
parts.controller.edit(); parts.controller.edit();
parts.controller.setPrivacy('shared'); parts.controller.setPrivacy('shared');
parts.controller.form.text = 'test'; parts.controller.form.text = 'test';
return parts.controller.save().then(function() { return parts.controller.save().then(function() {
assert(fakePermissions.setDefault.calledWithExactly('shared')); assert.calledWith(fakePermissions.setDefault, 'shared');
}); });
}); });
it('saves the "private" visibility level to localStorage', function() { it('saves the "private" visibility level to localStorage', function() {
var parts = createDirective(); var parts = createDirective();
parts.annotation.$update = sinon.stub().returns(Promise.resolve());
parts.controller.edit(); parts.controller.edit();
parts.controller.setPrivacy('private'); parts.controller.setPrivacy('private');
return parts.controller.save().then(function() { return parts.controller.save().then(function() {
assert(fakePermissions.setDefault.calledWithExactly('private')); assert.calledWith(fakePermissions.setDefault, 'private');
}); });
}); });
it('doesn\'t save the visibility if the annotation is a reply', function() { it('doesn\'t save the visibility if the annotation is a reply', function() {
var parts = createDirective(); var parts = createDirective();
parts.annotation.$update = sinon.stub().returns(Promise.resolve());
parts.annotation.references = ['parent id']; parts.annotation.references = ['parent id'];
parts.controller.edit(); parts.controller.edit();
parts.controller.setPrivacy('private'); parts.controller.setPrivacy('private');
return parts.controller.save().then(function() { return parts.controller.save().then(function() {
assert(!fakePermissions.setDefault.called); assert.notCalled(fakePermissions.setDefault);
}); });
}); });
}); });
...@@ -740,9 +720,8 @@ describe('annotation', function() { ...@@ -740,9 +720,8 @@ describe('annotation', function() {
sandbox.stub($window, 'confirm').returns(true); sandbox.stub($window, 'confirm').returns(true);
fakeAnnotationMapper.deleteAnnotation.returns($q.resolve()); fakeAnnotationMapper.deleteAnnotation.returns($q.resolve());
parts.controller['delete']().then(function() { parts.controller['delete']().then(function() {
assert( assert.calledWith(fakeAnnotationMapper.deleteAnnotation,
fakeAnnotationMapper.deleteAnnotation.calledWith( parts.annotation);
parts.annotation));
done(); done();
}); });
$timeout.flush(); $timeout.flush();
...@@ -771,8 +750,8 @@ describe('annotation', function() { ...@@ -771,8 +750,8 @@ describe('annotation', function() {
status: 0 status: 0
})); }));
controller['delete']().then(function() { controller['delete']().then(function() {
assert(fakeFlash.error.calledWith( assert.calledWith(fakeFlash.error,
'Service unreachable.', 'Deleting annotation failed')); 'Service unreachable.', 'Deleting annotation failed');
done(); done();
}); });
$timeout.flush(); $timeout.flush();
...@@ -788,8 +767,8 @@ describe('annotation', function() { ...@@ -788,8 +767,8 @@ describe('annotation', function() {
data: {} data: {}
})); }));
controller['delete']().then(function() { controller['delete']().then(function() {
assert(fakeFlash.error.calledWith( assert.calledWith(fakeFlash.error,
'500 Server Error', 'Deleting annotation failed')); '500 Server Error', 'Deleting annotation failed');
done(); done();
}); });
$timeout.flush(); $timeout.flush();
...@@ -800,7 +779,7 @@ describe('annotation', function() { ...@@ -800,7 +779,7 @@ describe('annotation', function() {
sandbox.stub($window, 'confirm').returns(true); sandbox.stub($window, 'confirm').returns(true);
fakeAnnotationMapper.deleteAnnotation.returns($q.resolve()); fakeAnnotationMapper.deleteAnnotation.returns($q.resolve());
controller['delete']().then(function() { controller['delete']().then(function() {
assert(fakeFlash.error.notCalled); assert.notCalled(fakeFlash.error);
done(); done();
}); });
$timeout.flush(); $timeout.flush();
...@@ -811,8 +790,7 @@ describe('annotation', function() { ...@@ -811,8 +790,7 @@ describe('annotation', function() {
var annotation; var annotation;
beforeEach(function() { beforeEach(function() {
annotation = fixtures.defaultAnnotation(); annotation = fixtures.newAnnotation();
annotation.$create = sandbox.stub();
}); });
function controllerWithActionCreate() { function controllerWithActionCreate() {
...@@ -824,63 +802,49 @@ describe('annotation', function() { ...@@ -824,63 +802,49 @@ describe('annotation', function() {
it( it(
'emits annotationCreated when saving an annotation succeeds', 'emits annotationCreated when saving an annotation succeeds',
function(done) { function() {
var controller = controllerWithActionCreate(); var controller = controllerWithActionCreate();
sandbox.spy($rootScope, '$emit'); sandbox.spy($rootScope, '$emit');
annotation.$create.returns(Promise.resolve()); return controller.save().then(function() {
controller.save().then(function() { assert.calledWith($rootScope.$emit, events.ANNOTATION_CREATED);
assert($rootScope.$emit.calledWith(events.ANNOTATION_CREATED));
done();
}); });
} }
); );
it( it('flashes a generic error if the server can\'t be reached', function() {
'flashes a generic error if the server can\'t be reached', var controller = controllerWithActionCreate();
function(done) { fakeStore.annotation.create = sinon.stub().returns(Promise.reject({
var controller = controllerWithActionCreate(); status: 0
annotation.$create.returns(Promise.reject({ }));
status: 0 return controller.save().then(function() {
})); assert.calledWith(fakeFlash.error,
controller.save().then(function() { 'Service unreachable.', 'Saving annotation failed');
assert(fakeFlash.error.calledWith( });
'Service unreachable.', 'Saving annotation failed')); });
done();
});
}
);
it( it('flashes an error if saving the annotation fails on the server', function() {
'flashes an error if saving the annotation fails on the server', var controller = controllerWithActionCreate();
function(done) { fakeStore.annotation.create = sinon.stub().returns(Promise.reject({
var controller = controllerWithActionCreate(); status: 500,
annotation.$create.returns(Promise.reject({ statusText: 'Server Error',
status: 500, data: {}
statusText: 'Server Error', }));
data: {} return controller.save().then(function() {
})); assert.calledWith(fakeFlash.error,
controller.save().then(function() { '500 Server Error', 'Saving annotation failed');
assert(fakeFlash.error.calledWith( });
'500 Server Error', 'Saving annotation failed')); });
done();
});
}
);
it( it('doesn\'t flash an error when saving an annotation succeeds', function() {
'doesn\'t flash an error when saving an annotation succeeds', var controller = controllerWithActionCreate();
function() { controller.save();
var controller = controllerWithActionCreate(); assert.notCalled(fakeFlash.error);
annotation.$create.returns(Promise.resolve()); });
controller.save();
assert(fakeFlash.error.notCalled);
}
);
it('shows a saving indicator when saving an annotation', function() { it('shows a saving indicator when saving an annotation', function() {
var controller = controllerWithActionCreate(); var controller = controllerWithActionCreate();
var create; var create;
annotation.$create.returns(new Promise(function (resolve) { fakeStore.annotation.create = sinon.stub().returns(new Promise(function (resolve) {
create = resolve; create = resolve;
})); }));
var saved = controller.save(); var saved = controller.save();
...@@ -895,7 +859,7 @@ describe('annotation', function() { ...@@ -895,7 +859,7 @@ describe('annotation', function() {
it('reverts to edit mode if saving fails', function () { it('reverts to edit mode if saving fails', function () {
var controller = controllerWithActionCreate(); var controller = controllerWithActionCreate();
var failCreation; var failCreation;
annotation.$create.returns(new Promise(function (resolve, reject) { fakeStore.annotation.create = sinon.stub().returns(new Promise(function (resolve, reject) {
failCreation = reject; failCreation = reject;
})); }));
var saved = controller.save(); var saved = controller.save();
...@@ -917,12 +881,11 @@ describe('annotation', function() { ...@@ -917,12 +881,11 @@ describe('annotation', function() {
user: 'acct:fred@hypothes.is', user: 'acct:fred@hypothes.is',
text: 'foo', text: 'foo',
}; };
annotation.$create = sinon.stub().returns(Promise.resolve());
var controller = createDirective(annotation).controller; var controller = createDirective(annotation).controller;
controller.action = 'create'; controller.action = 'create';
return controller.save().then(function() { return controller.save().then(function() {
assert.equal(annotation.$create.lastCall.thisValue.group, assert.calledWith(fakeStore.annotation.create, sinon.match({}),
'test-id'); sinon.match({group: 'test-id'}));
}); });
} }
); );
...@@ -933,7 +896,6 @@ describe('annotation', function() { ...@@ -933,7 +896,6 @@ describe('annotation', function() {
beforeEach(function() { beforeEach(function() {
annotation = fixtures.defaultAnnotation(); annotation = fixtures.defaultAnnotation();
annotation.$update = sandbox.stub();
}); });
function controllerWithActionEdit() { function controllerWithActionEdit() {
...@@ -947,12 +909,12 @@ describe('annotation', function() { ...@@ -947,12 +909,12 @@ describe('annotation', function() {
'flashes a generic error if the server cannot be reached', 'flashes a generic error if the server cannot be reached',
function() { function() {
var controller = controllerWithActionEdit(); var controller = controllerWithActionEdit();
annotation.$update.returns(Promise.reject({ fakeStore.annotation.update = sinon.stub().returns(Promise.reject({
status: -1 status: -1
})); }));
return controller.save().then(function() { return controller.save().then(function() {
assert(fakeFlash.error.calledWith( assert.calledWith(fakeFlash.error,
'Service unreachable.', 'Saving annotation failed')); 'Service unreachable.', 'Saving annotation failed');
}); });
} }
); );
...@@ -961,14 +923,14 @@ describe('annotation', function() { ...@@ -961,14 +923,14 @@ describe('annotation', function() {
'flashes an error if saving the annotation fails on the server', 'flashes an error if saving the annotation fails on the server',
function() { function() {
var controller = controllerWithActionEdit(); var controller = controllerWithActionEdit();
annotation.$update.returns(Promise.reject({ fakeStore.annotation.update = sinon.stub().returns(Promise.reject({
status: 500, status: 500,
statusText: 'Server Error', statusText: 'Server Error',
data: {} data: {}
})); }));
return controller.save().then(function() { return controller.save().then(function() {
assert(fakeFlash.error.calledWith( assert.calledWith(fakeFlash.error,
'500 Server Error', 'Saving annotation failed')); '500 Server Error', 'Saving annotation failed');
}); });
} }
); );
...@@ -977,10 +939,9 @@ describe('annotation', function() { ...@@ -977,10 +939,9 @@ describe('annotation', function() {
'doesn\'t flash an error if saving the annotation succeeds', 'doesn\'t flash an error if saving the annotation succeeds',
function() { function() {
var controller = controllerWithActionEdit(); var controller = controllerWithActionEdit();
annotation.$update.returns(Promise.resolve());
controller.form.text = 'updated text'; controller.form.text = 'updated text';
controller.save(); controller.save();
assert(fakeFlash.error.notCalled); assert.notCalled(fakeFlash.error);
} }
); );
}); });
...@@ -1014,7 +975,6 @@ describe('annotation', function() { ...@@ -1014,7 +975,6 @@ describe('annotation', function() {
it('removes the draft when changes are saved', function() { it('removes the draft when changes are saved', function() {
var annotation = fixtures.defaultAnnotation(); var annotation = fixtures.defaultAnnotation();
annotation.$update = sandbox.stub().returns(Promise.resolve());
var controller = createDirective(annotation).controller; var controller = createDirective(annotation).controller;
controller.edit(); controller.edit();
controller.form.text = 'test annotation'; controller.form.text = 'test annotation';
...@@ -1155,19 +1115,14 @@ describe('annotation', function() { ...@@ -1155,19 +1115,14 @@ describe('annotation', function() {
id: 'test-annotation-id', id: 'test-annotation-id',
user: 'acct:bill@localhost', user: 'acct:bill@localhost',
text: 'Initial annotation body text', text: 'Initial annotation body text',
// Allow the initial save of the annotation to succeed.
$create: function() {
return Promise.resolve();
},
// Simulate saving the edit of the annotation to the server failing.
$update: function() {
return Promise.reject({
status: 500,
statusText: 'Server Error',
data: {}
});
}
}).controller; }).controller;
fakeStore.annotation.update = function () {
return Promise.reject({
status: 500,
statusText: 'Server Error',
data: {}
});
};
var originalText = controller.form.text; var originalText = controller.form.text;
// Simulate the user clicking the Edit button on the annotation. // Simulate the user clicking the Edit button on the annotation.
controller.edit(); controller.edit();
...@@ -1178,10 +1133,10 @@ describe('annotation', function() { ...@@ -1178,10 +1133,10 @@ describe('annotation', function() {
controller.save(); controller.save();
// At this point the annotation editor controls are still open, and the // At this point the annotation editor controls are still open, and the
// annotation's text is still the modified (unsaved) text. // annotation's text is still the modified (unsaved) text.
assert(controller.form.text === 'changed by test code'); assert.equal(controller.form.text, 'changed by test code');
// Simulate the user clicking the Cancel button. // Simulate the user clicking the Cancel button.
controller.revert(); controller.revert();
assert(controller.form.text === originalText); assert.equal(controller.form.text, originalText);
}); });
// Test that editing reverting changes to an annotation with // Test that editing reverting changes to an annotation with
...@@ -1198,18 +1153,14 @@ describe('annotation', function() { ...@@ -1198,18 +1153,14 @@ describe('annotation', function() {
assert.equal(controller.form.text, ''); assert.equal(controller.form.text, '');
}); });
it('reverts to the most recently saved version', it('reverts to the most recently saved version', function () {
function () { fakeStore.annotation.update = function (params, ann) {
return Promise.resolve(Object.assign({}, ann));
};
var controller = createDirective({ var controller = createDirective({
id: 'new-annot',
user: 'acct:bill@localhost', user: 'acct:bill@localhost',
$create: function () {
this.id = 'new-annotation-id';
return Promise.resolve();
},
$update: function () {
return Promise.resolve(this);
},
}).controller; }).controller;
controller.edit(); controller.edit();
controller.form.text = 'New annotation text'; controller.form.text = 'New annotation text';
......
...@@ -8,15 +8,15 @@ var inherits = require('inherits'); ...@@ -8,15 +8,15 @@ var inherits = require('inherits');
* *
* SearchClient handles paging through results, canceling search etc. * SearchClient handles paging through results, canceling search etc.
* *
* @param {Object} resource - ngResource class instance for the /search API * @param {Object} searchFn - Function for querying the search API
* @param {Object} opts - Search options * @param {Object} opts - Search options
* @constructor * @constructor
*/ */
function SearchClient(resource, opts) { function SearchClient(searchFn, opts) {
opts = opts || {}; opts = opts || {};
var DEFAULT_CHUNK_SIZE = 200; var DEFAULT_CHUNK_SIZE = 200;
this._resource = resource; this._searchFn = searchFn;
this._chunkSize = opts.chunkSize || DEFAULT_CHUNK_SIZE; this._chunkSize = opts.chunkSize || DEFAULT_CHUNK_SIZE;
if (typeof opts.incremental !== 'undefined') { if (typeof opts.incremental !== 'undefined') {
this._incremental = opts.incremental; this._incremental = opts.incremental;
...@@ -37,7 +37,7 @@ SearchClient.prototype._getBatch = function (query, offset) { ...@@ -37,7 +37,7 @@ SearchClient.prototype._getBatch = function (query, offset) {
}, query); }, query);
var self = this; var self = this;
this._resource.get(searchQuery).$promise.then(function (results) { this._searchFn(searchQuery).then(function (results) {
if (self._canceled) { if (self._canceled) {
return; return;
} }
......
'use strict'; 'use strict';
var angular = require('angular'); var angular = require('angular');
var get = require('lodash.get');
var retryUtil = require('./retry-util'); var retryUtil = require('./retry-util');
var urlUtil = require('./util/url-util');
function prependTransform(defaults, transform) { function prependTransform(defaults, transform) {
// We can't guarantee that the default transformation is an array // We can't guarantee that the default transformation is an array
...@@ -79,55 +81,60 @@ function serializeParams(params) { ...@@ -79,55 +81,60 @@ function serializeParams(params) {
return parts.join('&'); return parts.join('&');
} }
/** /**
* @ngdoc factory * Creates a function that will make an API call to a named route.
* @name store
* @description The `store` service handles the backend calls for the restful
* API. This is created dynamically from the document returned at
* API index so as to ensure that URL paths/methods do not go out
* of date.
*
* The service currently exposes two resources:
* *
* store.SearchResource, for searching, and * @param $http - The Angular HTTP service
* store.AnnotationResource, for CRUD operations on annotations. * @param links - Object or promise for an object mapping named API routes to
* URL templates and methods
* @param route - The dotted path of the named API route (eg. `annotation.create`)
*/ */
// @ngInject function createAPICall($http, links, route) {
function store($http, $resource, settings) { return function (params, data) {
var instance = {}; return links.then(function (links) {
var defaultOptions = { var descriptor = get(links, route);
paramSerializer: serializeParams, var url = urlUtil.replaceURLParams(descriptor.url, params);
transformRequest: prependTransform( var req = {
$http.defaults.transformRequest, data: data,
stripInternalProperties method: descriptor.method,
) params: url.params,
paramSerializer: serializeParams,
url: url.url,
transformRequest: prependTransform(
$http.defaults.transformRequest,
stripInternalProperties
),
};
return $http(req);
}).then(function (result) {
return result.data;
});
}; };
}
// We call the API root and it gives back the actions it provides. /**
instance.$promise = retryUtil.retryPromiseOperation(function () { * API client for the Hypothesis REST API.
*
* Returns an object that with keys that match the routes in
* the Hypothesis API (see http://h.readthedocs.io/en/latest/api/).
*/
// @ngInject
function store($http, settings) {
var links = retryUtil.retryPromiseOperation(function () {
return $http.get(settings.apiUrl); return $http.get(settings.apiUrl);
}).then(function (response) { }).then(function (response) {
var links = response.data.links; return response.data.links;
// N.B. in both cases below we explicitly override the default `get`
// action because there is no way to provide defaultOptions to the default
// action.
instance.SearchResource = $resource(links.search.url, {}, {
get: angular.extend({url: links.search.url}, defaultOptions),
});
instance.AnnotationResource = $resource(links.annotation.read.url, {}, {
get: angular.extend(links.annotation.read, defaultOptions),
create: angular.extend(links.annotation.create, defaultOptions),
update: angular.extend(links.annotation.update, defaultOptions),
delete: angular.extend(links.annotation.delete, defaultOptions),
});
return instance;
}); });
return instance; return {
search: createAPICall($http, links, 'search'),
annotation: {
create: createAPICall($http, links, 'annotation.create'),
delete: createAPICall($http, links, 'annotation.delete'),
get: createAPICall($http, links, 'annotation.read'),
update: createAPICall($http, links, 'annotation.update'),
},
};
} }
module.exports = store; module.exports = store;
...@@ -20,11 +20,13 @@ module.exports = class StreamController ...@@ -20,11 +20,13 @@ module.exports = class StreamController
searchParams = searchFilter.toObject($routeParams.q) searchParams = searchFilter.toObject($routeParams.q)
query = angular.extend(options, searchParams) query = angular.extend(options, searchParams)
query._separate_replies = true query._separate_replies = true
store.SearchResource.get(query, load) store.search(query)
.then(load)
.catch((err) -> console.error err)
load = ({rows, replies}) -> load = ({rows, replies}) ->
offset += rows.length offset += rows.length
annotationMapper.loadAnnotations(rows, replies) annotationMapper.loadAnnotations(rows, replies)
# Reload on query change (ignore hash change) # Reload on query change (ignore hash change)
lastQuery = $routeParams.q lastQuery = $routeParams.q
......
...@@ -13,7 +13,9 @@ describe('annotationMapper', function() { ...@@ -13,7 +13,9 @@ describe('annotationMapper', function() {
beforeEach(function () { beforeEach(function () {
fakeStore = { fakeStore = {
AnnotationResource: sandbox.stub().returns({}), annotation: {
delete: sinon.stub().returns(Promise.resolve({})),
},
}; };
angular.module('app', []) angular.module('app', [])
.service('annotationMapper', require('../annotation-mapper')) .service('annotationMapper', require('../annotation-mapper'))
...@@ -40,7 +42,7 @@ describe('annotationMapper', function() { ...@@ -40,7 +42,7 @@ describe('annotationMapper', function() {
annotationMapper.loadAnnotations(annotations); annotationMapper.loadAnnotations(annotations);
assert.called($rootScope.$emit); assert.called($rootScope.$emit);
assert.calledWith($rootScope.$emit, events.ANNOTATIONS_LOADED, assert.calledWith($rootScope.$emit, events.ANNOTATIONS_LOADED,
[{}, {}, {}]); [{id: 1}, {id: 2}, {id: 3}]);
}); });
it('also includes replies in the annotationLoaded event', function () { it('also includes replies in the annotationLoaded event', function () {
...@@ -50,7 +52,7 @@ describe('annotationMapper', function() { ...@@ -50,7 +52,7 @@ describe('annotationMapper', function() {
annotationMapper.loadAnnotations(annotations, replies); annotationMapper.loadAnnotations(annotations, replies);
assert.called($rootScope.$emit); assert.called($rootScope.$emit);
assert.calledWith($rootScope.$emit, events.ANNOTATIONS_LOADED, assert.calledWith($rootScope.$emit, events.ANNOTATIONS_LOADED,
[{}, {}, {}]); [{id: 1}, {id: 2}, {id: 3}]);
}); });
it('triggers the annotationUpdated event for each loaded annotation', function () { it('triggers the annotationUpdated event for each loaded annotation', function () {
...@@ -122,25 +124,16 @@ describe('annotationMapper', function() { ...@@ -122,25 +124,16 @@ describe('annotationMapper', function() {
}); });
describe('#createAnnotation()', function () { describe('#createAnnotation()', function () {
it('creates a new annotaton resource', function () { it('creates a new annotation resource', function () {
var ann = {}; var ann = {};
fakeStore.AnnotationResource.returns(ann);
var ret = annotationMapper.createAnnotation(ann); var ret = annotationMapper.createAnnotation(ann);
assert.equal(ret, ann); assert.equal(ret, ann);
}); });
it('creates a new resource with the new keyword', function () {
var ann = {};
fakeStore.AnnotationResource.returns(ann);
annotationMapper.createAnnotation();
assert.calledWithNew(fakeStore.AnnotationResource);
});
it('emits the "beforeAnnotationCreated" event', function () { it('emits the "beforeAnnotationCreated" event', function () {
sandbox.stub($rootScope, '$emit'); sandbox.stub($rootScope, '$emit');
var ann = {}; var ann = {};
fakeStore.AnnotationResource.returns(ann); annotationMapper.createAnnotation(ann);
annotationMapper.createAnnotation();
assert.calledWith($rootScope.$emit, assert.calledWith($rootScope.$emit,
events.BEFORE_ANNOTATION_CREATED, ann); events.BEFORE_ANNOTATION_CREATED, ann);
}); });
...@@ -148,16 +141,14 @@ describe('annotationMapper', function() { ...@@ -148,16 +141,14 @@ describe('annotationMapper', function() {
describe('#deleteAnnotation()', function () { describe('#deleteAnnotation()', function () {
it('deletes the annotation on the server', function () { it('deletes the annotation on the server', function () {
var p = Promise.resolve(); var ann = {id: 'test-id'};
var ann = {$delete: sandbox.stub().returns(p)};
annotationMapper.deleteAnnotation(ann); annotationMapper.deleteAnnotation(ann);
assert.called(ann.$delete); assert.calledWith(fakeStore.annotation.delete, {id: 'test-id'});
}); });
it('triggers the "annotationDeleted" event on success', function (done) { it('triggers the "annotationDeleted" event on success', function (done) {
sandbox.stub($rootScope, '$emit'); sandbox.stub($rootScope, '$emit');
var p = Promise.resolve(); var ann = {};
var ann = {$delete: sandbox.stub().returns(p)};
annotationMapper.deleteAnnotation(ann).then(function () { annotationMapper.deleteAnnotation(ann).then(function () {
assert.calledWith($rootScope.$emit, assert.calledWith($rootScope.$emit,
events.ANNOTATION_DELETED, ann); events.ANNOTATION_DELETED, ann);
...@@ -165,23 +156,14 @@ describe('annotationMapper', function() { ...@@ -165,23 +156,14 @@ describe('annotationMapper', function() {
$rootScope.$apply(); $rootScope.$apply();
}); });
it('does nothing on error', function (done) { it('does not emit an event on error', function (done) {
sandbox.stub($rootScope, '$emit'); sandbox.stub($rootScope, '$emit');
var p = Promise.reject(); fakeStore.annotation.delete.returns(Promise.reject());
var ann = {$delete: sandbox.stub().returns(p)}; var ann = {id: 'test-id'};
annotationMapper.deleteAnnotation(ann).catch(function () { annotationMapper.deleteAnnotation(ann).catch(function () {
assert.notCalled($rootScope.$emit); assert.notCalled($rootScope.$emit);
}).then(done, done); }).then(done, done);
$rootScope.$apply(); $rootScope.$apply();
}); });
it('return a promise that resolves to the deleted annotation', function (done) {
var p = Promise.resolve();
var ann = {$delete: sandbox.stub().returns(p)};
annotationMapper.deleteAnnotation(ann).then(function (value) {
assert.equal(value, ann);
}).then(done, done);
$rootScope.$apply();
});
}); });
}); });
...@@ -7,7 +7,7 @@ var angular = require('angular'); ...@@ -7,7 +7,7 @@ var angular = require('angular');
function FakeStore(annots) { function FakeStore(annots) {
this.annots = annots; this.annots = annots;
this.AnnotationResource = { this.annotation = {
get: function (query) { get: function (query) {
var result; var result;
if (query.id) { if (query.id) {
...@@ -15,20 +15,18 @@ function FakeStore(annots) { ...@@ -15,20 +15,18 @@ function FakeStore(annots) {
return a.id === query.id; return a.id === query.id;
}); });
} }
return {$promise: Promise.resolve(result)}; return Promise.resolve(result);
} }
}; };
this.SearchResource = { this.search = function (query) {
get: function (query) { var result;
var result; if (query.references) {
if (query.references) { result = annots.filter(function (a) {
result = annots.filter(function (a) { return a.references && a.references.indexOf(query.references) !== -1;
return a.references && a.references.indexOf(query.references) !== -1; });
});
}
return {$promise: Promise.resolve({rows: result})};
} }
return Promise.resolve({rows: result});
}; };
} }
......
...@@ -16,24 +16,20 @@ describe('SearchClient', function () { ...@@ -16,24 +16,20 @@ describe('SearchClient', function () {
{id: 'four'}, {id: 'four'},
]; ];
var fakeResource; var fakeSearchFn;
beforeEach(function () { beforeEach(function () {
fakeResource = { fakeSearchFn = sinon.spy(function (params) {
get: sinon.spy(function (params) { return Promise.resolve({
return { rows: RESULTS.slice(params.offset,
$promise: Promise.resolve({ params.offset + params.limit),
rows: RESULTS.slice(params.offset, total: RESULTS.length,
params.offset + params.limit), });
total: RESULTS.length, });
}),
};
}),
};
}); });
it('emits "results"', function () { it('emits "results"', function () {
var client = new SearchClient(fakeResource); var client = new SearchClient(fakeSearchFn);
var onResults = sinon.stub(); var onResults = sinon.stub();
client.on('results', onResults); client.on('results', onResults);
client.get({uri: 'http://example.com'}); client.get({uri: 'http://example.com'});
...@@ -43,7 +39,7 @@ describe('SearchClient', function () { ...@@ -43,7 +39,7 @@ describe('SearchClient', function () {
}); });
it('emits "results" with chunks in incremental mode', function () { it('emits "results" with chunks in incremental mode', function () {
var client = new SearchClient(fakeResource, {chunkSize: 2}); var client = new SearchClient(fakeSearchFn, {chunkSize: 2});
var onResults = sinon.stub(); var onResults = sinon.stub();
client.on('results', onResults); client.on('results', onResults);
client.get({uri: 'http://example.com'}); client.get({uri: 'http://example.com'});
...@@ -54,7 +50,7 @@ describe('SearchClient', function () { ...@@ -54,7 +50,7 @@ describe('SearchClient', function () {
}); });
it('emits "results" once in non-incremental mode', function () { it('emits "results" once in non-incremental mode', function () {
var client = new SearchClient(fakeResource, var client = new SearchClient(fakeSearchFn,
{chunkSize: 2, incremental: false}); {chunkSize: 2, incremental: false});
var onResults = sinon.stub(); var onResults = sinon.stub();
client.on('results', onResults); client.on('results', onResults);
...@@ -66,7 +62,7 @@ describe('SearchClient', function () { ...@@ -66,7 +62,7 @@ describe('SearchClient', function () {
}); });
it('does not emit "results" if canceled', function () { it('does not emit "results" if canceled', function () {
var client = new SearchClient(fakeResource); var client = new SearchClient(fakeSearchFn);
var onResults = sinon.stub(); var onResults = sinon.stub();
var onEnd = sinon.stub(); var onEnd = sinon.stub();
client.on('results', onResults); client.on('results', onResults);
...@@ -81,12 +77,10 @@ describe('SearchClient', function () { ...@@ -81,12 +77,10 @@ describe('SearchClient', function () {
it('emits "error" event if search fails', function () { it('emits "error" event if search fails', function () {
var err = new Error('search failed'); var err = new Error('search failed');
fakeResource.get = function () { fakeSearchFn = function () {
return { return Promise.reject(err);
$promise: Promise.reject(err),
};
}; };
var client = new SearchClient(fakeResource); var client = new SearchClient(fakeSearchFn);
var onError = sinon.stub(); var onError = sinon.stub();
client.on('error', onError); client.on('error', onError);
client.get({uri: 'http://example.com'}); client.get({uri: 'http://example.com'});
......
...@@ -11,7 +11,7 @@ describe('store', function () { ...@@ -11,7 +11,7 @@ describe('store', function () {
var store = null; var store = null;
before(function () { before(function () {
angular.module('h', ['ngResource']) angular.module('h')
.service('store', proxyquire('../store', util.noCallThru({ .service('store', proxyquire('../store', util.noCallThru({
angular: angular, angular: angular,
'./retry-util': { './retry-util': {
...@@ -35,7 +35,7 @@ describe('store', function () { ...@@ -35,7 +35,7 @@ describe('store', function () {
sandbox.restore(); sandbox.restore();
}); });
beforeEach(angular.mock.inject(function ($q, _$httpBackend_, _store_) { beforeEach(angular.mock.inject(function (_$httpBackend_, _store_) {
$httpBackend = _$httpBackend_; $httpBackend = _$httpBackend_;
store = _store_; store = _store_;
...@@ -46,11 +46,18 @@ describe('store', function () { ...@@ -46,11 +46,18 @@ describe('store', function () {
method: 'POST', method: 'POST',
url: 'http://example.com/api/annotations', url: 'http://example.com/api/annotations',
}, },
delete: {}, delete: {
method: 'DELETE',
url: 'http://example.com/api/annotations/:id',
},
read: {}, read: {},
update: {}, update: {
method: 'PUT',
url: 'http://example.com/api/annotations/:id',
},
}, },
search: { search: {
method: 'GET',
url: 'http://example.com/api/search', url: 'http://example.com/api/search',
}, },
}, },
...@@ -58,34 +65,42 @@ describe('store', function () { ...@@ -58,34 +65,42 @@ describe('store', function () {
$httpBackend.flush(); $httpBackend.flush();
})); }));
it('reads the operations from the backend', function () {
assert.isFunction(store.AnnotationResource);
assert.isFunction(store.SearchResource);
});
it('saves a new annotation', function () { it('saves a new annotation', function () {
var annotation = new store.AnnotationResource({id: 'test'}); store.annotation.create({}, {}).then(function (saved) {
var saved = {};
annotation.$create().then(function () {
assert.isNotNull(saved.id); assert.isNotNull(saved.id);
}); });
$httpBackend.expectPOST('http://example.com/api/annotations')
.respond(function () {
return [201, {id: 'new-id'}, {}];
});
$httpBackend.flush();
});
$httpBackend.expectPOST('http://example.com/api/annotations', {id: 'test'}) it('updates an annotation', function () {
store.annotation.update({id: 'an-id'}, {text: 'updated'});
$httpBackend.expectPUT('http://example.com/api/annotations/an-id')
.respond(function () { .respond(function () {
saved.id = annotation.id; return [200, {}, {}];
return [201, {}, {}]; });
$httpBackend.flush();
});
it('deletes an annotation', function () {
store.annotation.delete({id: 'an-id'}, {});
$httpBackend.expectDELETE('http://example.com/api/annotations/an-id')
.respond(function () {
return [200, {}, {}];
}); });
$httpBackend.flush(); $httpBackend.flush();
}); });
it('removes internal properties before sending data to the server', function () { it('removes internal properties before sending data to the server', function () {
var annotation = new store.AnnotationResource({ var annotation = {
$highlight: true, $highlight: true,
$notme: 'nooooo!', $notme: 'nooooo!',
allowed: 123 allowed: 123
}); };
annotation.$create(); store.annotation.create({}, annotation);
$httpBackend.expectPOST('http://example.com/api/annotations', { $httpBackend.expectPOST('http://example.com/api/annotations', {
allowed: 123 allowed: 123
}) })
...@@ -96,7 +111,7 @@ describe('store', function () { ...@@ -96,7 +111,7 @@ describe('store', function () {
// Our backend service interprets semicolons as query param delimiters, so we // Our backend service interprets semicolons as query param delimiters, so we
// must ensure to encode them in the query string. // must ensure to encode them in the query string.
it('encodes semicolons in query parameters', function () { it('encodes semicolons in query parameters', function () {
store.SearchResource.get({'uri': 'http://example.com/?foo=bar;baz=qux'}); store.search({'uri': 'http://example.com/?foo=bar;baz=qux'});
$httpBackend.expectGET('http://example.com/api/search?uri=http%3A%2F%2Fexample.com%2F%3Ffoo%3Dbar%3Bbaz%3Dqux') $httpBackend.expectGET('http://example.com/api/search?uri=http%3A%2F%2Fexample.com%2F%3Ffoo%3Dbar%3Bbaz%3Dqux')
.respond(function () { return [200, {}, {}]; }); .respond(function () { return [200, {}, {}]; });
$httpBackend.flush(); $httpBackend.flush();
......
...@@ -62,9 +62,7 @@ describe 'StreamController', -> ...@@ -62,9 +62,7 @@ describe 'StreamController', ->
} }
fakeStore = { fakeStore = {
SearchResource: { search: sandbox.spy(-> Promise.resolve({rows: [], total: 0}))
get: sandbox.spy()
}
} }
fakeStreamer = { fakeStreamer = {
...@@ -103,22 +101,21 @@ describe 'StreamController', -> ...@@ -103,22 +101,21 @@ describe 'StreamController', ->
it 'calls the search API with _separate_replies: true', -> it 'calls the search API with _separate_replies: true', ->
createController() createController()
assert.equal( assert.equal(fakeStore.search.firstCall.args[0]._separate_replies, true)
fakeStore.SearchResource.get.firstCall.args[0]._separate_replies, true)
it 'passes the annotations and replies from search to loadAnnotations()', -> it 'passes the annotations and replies from search to loadAnnotations()', ->
fakeStore.SearchResource.get = (query, func) -> fakeStore.search = (query) ->
func({ Promise.resolve({
'rows': ['annotation_1', 'annotation_2'] 'rows': ['annotation_1', 'annotation_2']
'replies': ['reply_1', 'reply_2', 'reply_3'] 'replies': ['reply_1', 'reply_2', 'reply_3']
}) })
createController() createController()
assert fakeAnnotationMapper.loadAnnotations.calledOnce Promise.resolve().then ->
assert fakeAnnotationMapper.loadAnnotations.calledWith( assert.calledOnce fakeAnnotationMapper.loadAnnotations
['annotation_1', 'annotation_2'], ['reply_1', 'reply_2', 'reply_3'] assert.calledWith fakeAnnotationMapper.loadAnnotations,
) ['annotation_1', 'annotation_2'], ['reply_1', 'reply_2', 'reply_3']
describe 'on $routeUpdate', -> describe 'on $routeUpdate', ->
......
...@@ -9,8 +9,8 @@ var events = require('../events'); ...@@ -9,8 +9,8 @@ var events = require('../events');
var noCallThru = require('./util').noCallThru; var noCallThru = require('./util').noCallThru;
var searchClients; var searchClients;
function FakeSearchClient(resource, opts) { function FakeSearchClient(searchFn, opts) {
assert.ok(resource); assert.ok(searchFn);
searchClients.push(this); searchClients.push(this);
this.cancel = sinon.stub(); this.cancel = sinon.stub();
this.incremental = !!opts.incremental; this.incremental = !!opts.incremental;
...@@ -118,7 +118,7 @@ describe('WidgetController', function () { ...@@ -118,7 +118,7 @@ describe('WidgetController', function () {
fakeSettings = {}; fakeSettings = {};
fakeStore = { fakeStore = {
SearchResource: {}, search: sinon.stub(),
}; };
$provide.value('VirtualThreadList', FakeVirtualThreadList); $provide.value('VirtualThreadList', FakeVirtualThreadList);
......
'use strict';
var urlUtil = require('../url-util');
describe('url-util', function () {
describe('replaceURLParams()', function () {
it('should replace params in URLs', function () {
var replaced = urlUtil.replaceURLParams('http://foo.com/things/:id',
{id: 'test'});
assert.equal(replaced.url, 'http://foo.com/things/test');
});
it('should return unused params', function () {
var replaced = urlUtil.replaceURLParams('http://foo.com/:id',
{id: 'test', 'q': 'unused'});
assert.deepEqual(replaced.params, {q: 'unused'});
});
});
});
'use strict';
/**
* Replace parameters in a URL template with values from a `params` object.
*
* Returns an object containing the expanded URL and a dictionary of unused
* parameters.
*
* replaceURLParams('/things/:id', {id: 'foo', q: 'bar'}) =>
* {url: '/things/foo', params: {q: 'bar'}}
*/
function replaceURLParams(url, params) {
var unusedParams = {};
for (var param in params) {
if (params.hasOwnProperty(param)) {
var value = params[param];
var urlParam = ':' + param;
if (url.indexOf(urlParam) !== -1) {
url = url.replace(urlParam, value);
} else {
unusedParams[param] = value;
}
}
}
return {url: url, params: unusedParams};
}
module.exports = {
replaceURLParams: replaceURLParams,
};
...@@ -189,7 +189,7 @@ module.exports = function WidgetController( ...@@ -189,7 +189,7 @@ module.exports = function WidgetController(
} }
function _loadAnnotationsFor(uris, group) { function _loadAnnotationsFor(uris, group) {
var searchClient = new SearchClient(store.SearchResource, { var searchClient = new SearchClient(store.search, {
// If no group is specified, we are fetching annotations from // If no group is specified, we are fetching annotations from
// all groups in order to find out which group contains the selected // all groups in order to find out which group contains the selected
// annotation, therefore we need to load all chunks before processing // annotation, therefore we need to load all chunks before processing
......
...@@ -61,6 +61,7 @@ ...@@ -61,6 +61,7 @@
"karma-phantomjs-launcher": "^0.2.3", "karma-phantomjs-launcher": "^0.2.3",
"karma-sinon": "^1.0.4", "karma-sinon": "^1.0.4",
"lodash.debounce": "^4.0.3", "lodash.debounce": "^4.0.3",
"lodash.get": "^4.3.0",
"mkdirp": "^0.5.1", "mkdirp": "^0.5.1",
"mocha": "^2.4.5", "mocha": "^2.4.5",
"ng-tags-input": "^3.1.1", "ng-tags-input": "^3.1.1",
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment