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
......
...@@ -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