Unverified Commit 056a32bb authored by Lyza Gardner's avatar Lyza Gardner Committed by GitHub

Merge pull request #1875 from hypothesis/split-annotations-service

Split `loadAnnotationsService` out of `annotationsService`
parents ac8fcfbc c1b99fec
...@@ -6,7 +6,7 @@ import * as tabs from '../util/tabs'; ...@@ -6,7 +6,7 @@ import * as tabs from '../util/tabs';
function SidebarContentController( function SidebarContentController(
$scope, $scope,
analytics, analytics,
annotationsService, loadAnnotationsService,
store, store,
frameSync, frameSync,
rootThread, rootThread,
...@@ -120,7 +120,7 @@ function SidebarContentController( ...@@ -120,7 +120,7 @@ function SidebarContentController(
} }
const searchUris = store.searchUris(); const searchUris = store.searchUris();
annotationsService.load(searchUris, currentGroupId); loadAnnotationsService.load(searchUris, currentGroupId);
}, },
true true
); );
......
...@@ -20,7 +20,7 @@ describe('sidebar.components.sidebar-content', function() { ...@@ -20,7 +20,7 @@ describe('sidebar.components.sidebar-content', function() {
let store; let store;
let ctrl; let ctrl;
let fakeAnalytics; let fakeAnalytics;
let fakeAnnotations; let fakeLoadAnnotationsService;
let fakeFrameSync; let fakeFrameSync;
let fakeRootThread; let fakeRootThread;
let fakeSettings; let fakeSettings;
...@@ -56,7 +56,7 @@ describe('sidebar.components.sidebar-content', function() { ...@@ -56,7 +56,7 @@ describe('sidebar.components.sidebar-content', function() {
reconnect: sandbox.stub(), reconnect: sandbox.stub(),
}; };
fakeAnnotations = { fakeLoadAnnotationsService = {
load: sinon.stub(), load: sinon.stub(),
}; };
...@@ -68,7 +68,7 @@ describe('sidebar.components.sidebar-content', function() { ...@@ -68,7 +68,7 @@ describe('sidebar.components.sidebar-content', function() {
$provide.value('frameSync', fakeFrameSync); $provide.value('frameSync', fakeFrameSync);
$provide.value('rootThread', fakeRootThread); $provide.value('rootThread', fakeRootThread);
$provide.value('streamer', fakeStreamer); $provide.value('streamer', fakeStreamer);
$provide.value('annotationsService', fakeAnnotations); $provide.value('loadAnnotationsService', fakeLoadAnnotationsService);
$provide.value('settings', fakeSettings); $provide.value('settings', fakeSettings);
}); });
}); });
...@@ -167,7 +167,7 @@ describe('sidebar.components.sidebar-content', function() { ...@@ -167,7 +167,7 @@ describe('sidebar.components.sidebar-content', function() {
function connectFrameAndPerformInitialFetch() { function connectFrameAndPerformInitialFetch() {
setFrames([{ uri: 'https://a-page.com' }]); setFrames([{ uri: 'https://a-page.com' }]);
$scope.$digest(); $scope.$digest();
fakeAnnotations.load.reset(); fakeLoadAnnotationsService.load.reset();
} }
it('generates the thread list', () => { it('generates the thread list', () => {
...@@ -184,7 +184,7 @@ describe('sidebar.components.sidebar-content', function() { ...@@ -184,7 +184,7 @@ describe('sidebar.components.sidebar-content', function() {
$scope.$digest(); $scope.$digest();
assert.calledWith( assert.calledWith(
fakeAnnotations.load, fakeLoadAnnotationsService.load,
['https://a-page.com', 'https://new-frame.com'], ['https://a-page.com', 'https://new-frame.com'],
'group-id' 'group-id'
); );
...@@ -203,7 +203,7 @@ describe('sidebar.components.sidebar-content', function() { ...@@ -203,7 +203,7 @@ describe('sidebar.components.sidebar-content', function() {
$scope.$digest(); $scope.$digest();
assert.calledWith( assert.calledWith(
fakeAnnotations.load, fakeLoadAnnotationsService.load,
['https://a-page.com'], ['https://a-page.com'],
'group-id' 'group-id'
); );
...@@ -219,7 +219,7 @@ describe('sidebar.components.sidebar-content', function() { ...@@ -219,7 +219,7 @@ describe('sidebar.components.sidebar-content', function() {
store.updateSession(newProfile); store.updateSession(newProfile);
$scope.$digest(); $scope.$digest();
assert.notCalled(fakeAnnotations.load); assert.notCalled(fakeLoadAnnotationsService.load);
}); });
}); });
...@@ -250,7 +250,7 @@ describe('sidebar.components.sidebar-content', function() { ...@@ -250,7 +250,7 @@ describe('sidebar.components.sidebar-content', function() {
store.addAnnotations = sinon.stub(); store.addAnnotations = sinon.stub();
setFrames([{ uri: uri }]); setFrames([{ uri: uri }]);
$scope.$digest(); $scope.$digest();
fakeAnnotations.load = sinon.stub(); fakeLoadAnnotationsService.load = sinon.stub();
}); });
it('should load annotations for the new group', () => { it('should load annotations for the new group', () => {
...@@ -260,7 +260,7 @@ describe('sidebar.components.sidebar-content', function() { ...@@ -260,7 +260,7 @@ describe('sidebar.components.sidebar-content', function() {
$scope.$digest(); $scope.$digest();
assert.calledWith( assert.calledWith(
fakeAnnotations.load, fakeLoadAnnotationsService.load,
['http://example.com'], ['http://example.com'],
'different-group' 'different-group'
); );
......
...@@ -179,6 +179,7 @@ import featuresService from './services/features'; ...@@ -179,6 +179,7 @@ import featuresService from './services/features';
import flashService from './services/flash'; import flashService from './services/flash';
import frameSyncService from './services/frame-sync'; import frameSyncService from './services/frame-sync';
import groupsService from './services/groups'; import groupsService from './services/groups';
import loadAnnotationsService from './services/load-annotations';
import localStorageService from './services/local-storage'; import localStorageService from './services/local-storage';
import permissionsService from './services/permissions'; import permissionsService from './services/permissions';
import persistedDefaultsService from './services/persisted-defaults'; import persistedDefaultsService from './services/persisted-defaults';
...@@ -221,6 +222,7 @@ function startAngularApp(config) { ...@@ -221,6 +222,7 @@ function startAngularApp(config) {
.register('flash', flashService) .register('flash', flashService)
.register('frameSync', frameSyncService) .register('frameSync', frameSyncService)
.register('groups', groupsService) .register('groups', groupsService)
.register('loadAnnotationsService', loadAnnotationsService)
.register('localStorage', localStorageService) .register('localStorage', localStorageService)
.register('permissions', permissionsService) .register('permissions', permissionsService)
.register('persistedDefaults', persistedDefaultsService) .register('persistedDefaults', persistedDefaultsService)
...@@ -307,6 +309,9 @@ function startAngularApp(config) { ...@@ -307,6 +309,9 @@ function startAngularApp(config) {
.service('flash', () => container.get('flash')) .service('flash', () => container.get('flash'))
.service('frameSync', () => container.get('frameSync')) .service('frameSync', () => container.get('frameSync'))
.service('groups', () => container.get('groups')) .service('groups', () => container.get('groups'))
.service('loadAnnotationsService', () =>
container.get('loadAnnotationsService')
)
.service('permissions', () => container.get('permissions')) .service('permissions', () => container.get('permissions'))
.service('persistedDefaults', () => container.get('persistedDefaults')) .service('persistedDefaults', () => container.get('persistedDefaults'))
.service('rootThread', () => container.get('rootThread')) .service('rootThread', () => container.get('rootThread'))
......
import SearchClient from '../search-client'; /**
* A service for creating, manipulating and persisting annotations and their
* application-store representations. Interacts with API services as needed.
*/
import * as metadata from '../util/annotation-metadata'; import * as metadata from '../util/annotation-metadata';
import { import {
defaultPermissions, defaultPermissions,
...@@ -9,14 +13,26 @@ import { generateHexString } from '../util/random'; ...@@ -9,14 +13,26 @@ import { generateHexString } from '../util/random';
import uiConstants from '../ui-constants'; import uiConstants from '../ui-constants';
// @ngInject // @ngInject
export default function annotationsService( export default function annotationsService(api, store) {
annotationMapper, /**
api, * Apply changes for the given `annotation` from its draft in the store (if
store, * any) and return a new object with those changes integrated.
streamer, */
streamFilter function applyDraftChanges(annotation) {
) { const changes = {};
let searchClient = null; const draft = store.getDraft(annotation);
if (draft) {
changes.tags = draft.tags;
changes.text = draft.text;
changes.permissions = draft.isPrivate
? privatePermissions(annotation.user)
: sharedPermissions(annotation.user, annotation.group);
}
// Integrate changes from draft into object to be persisted
return { ...annotation, ...changes };
}
/** /**
* Extend new annotation objects with defaults and permissions. * Extend new annotation objects with defaults and permissions.
...@@ -100,75 +116,6 @@ export default function annotationsService( ...@@ -100,75 +116,6 @@ export default function annotationsService(
}); });
} }
/**
* Load annotations for all URIs and groupId.
*
* @param {string[]} uris
* @param {string} groupId
*/
function load(uris, groupId) {
annotationMapper.unloadAnnotations(store.savedAnnotations());
// Cancel previously running search client.
if (searchClient) {
searchClient.cancel();
}
if (uris.length > 0) {
searchAndLoad(uris, groupId);
streamFilter.resetFilter().addClause('/uri', 'one_of', uris);
streamer.setConfig('filter', { filter: streamFilter.getFilter() });
}
}
function searchAndLoad(uris, groupId) {
searchClient = new SearchClient(api.search, {
incremental: true,
});
searchClient.on('results', results => {
if (results.length) {
annotationMapper.loadAnnotations(results);
}
});
searchClient.on('error', error => {
console.error(error);
});
searchClient.on('end', () => {
// Remove client as it's no longer active.
searchClient = null;
store.frames().forEach(function(frame) {
if (0 <= uris.indexOf(frame.uri)) {
store.updateFrameAnnotationFetchStatus(frame.uri, true);
}
});
store.annotationFetchFinished();
});
store.annotationFetchStarted();
searchClient.get({ uri: uris, group: groupId });
}
/**
* Apply changes for the given `annotation` from its draft in the store (if
* any) and return a new object with those changes integrated.
*/
function applyDraftChanges(annotation) {
const changes = {};
const draft = store.getDraft(annotation);
if (draft) {
changes.tags = draft.tags;
changes.text = draft.text;
changes.permissions = draft.isPrivate
? privatePermissions(annotation.user)
: sharedPermissions(annotation.user, annotation.group);
}
// Integrate changes from draft into object to be persisted
return { ...annotation, ...changes };
}
/** /**
* Create a reply to `annotation` by the user `userid` and add to the store. * Create a reply to `annotation` by the user `userid` and add to the store.
* *
...@@ -225,7 +172,6 @@ export default function annotationsService( ...@@ -225,7 +172,6 @@ export default function annotationsService(
return { return {
create, create,
load,
reply, reply,
save, save,
}; };
......
/**
* A service for fetching annotations, filtered by document URIs and group.
*/
import SearchClient from '../search-client';
// @ngInject
export default function loadAnnotationsService(
annotationMapper,
api,
store,
streamer,
streamFilter
) {
let searchClient = null;
/**
* Load annotations for all URIs and groupId.
*
* @param {string[]} uris
* @param {string} groupId
*/
function load(uris, groupId) {
annotationMapper.unloadAnnotations(store.savedAnnotations());
// Cancel previously running search client.
if (searchClient) {
searchClient.cancel();
}
if (uris.length > 0) {
searchAndLoad(uris, groupId);
streamFilter.resetFilter().addClause('/uri', 'one_of', uris);
streamer.setConfig('filter', { filter: streamFilter.getFilter() });
}
}
function searchAndLoad(uris, groupId) {
searchClient = new SearchClient(api.search, {
incremental: true,
});
searchClient.on('results', results => {
if (results.length) {
annotationMapper.loadAnnotations(results);
}
});
searchClient.on('error', error => {
console.error(error);
});
searchClient.on('end', () => {
// Remove client as it's no longer active.
searchClient = null;
store.frames().forEach(function(frame) {
if (0 <= uris.indexOf(frame.uri)) {
store.updateFrameAnnotationFetchStatus(frame.uri, true);
}
});
store.annotationFetchFinished();
});
store.annotationFetchStarted();
searchClient.get({ uri: uris, group: groupId });
}
return {
load,
};
}
import EventEmitter from 'tiny-emitter';
import * as fixtures from '../../test/annotation-fixtures'; import * as fixtures from '../../test/annotation-fixtures';
import uiConstants from '../../ui-constants'; import uiConstants from '../../ui-constants';
import annotationsService from '../annotations'; import annotationsService, { $imports } from '../annotations';
import { $imports } from '../annotations';
let searchClients;
let longRunningSearchClient = false;
class FakeSearchClient extends EventEmitter {
constructor(searchFn, opts) {
super();
assert.ok(searchFn);
searchClients.push(this);
this.cancel = sinon.stub();
this.incremental = !!opts.incremental;
this.get = sinon.spy(query => {
assert.ok(query.uri);
for (let i = 0; i < query.uri.length; i++) {
const uri = query.uri[i];
this.emit('results', [{ id: uri + '123', group: '__world__' }]);
this.emit('results', [{ id: uri + '456', group: 'private-group' }]);
}
if (!longRunningSearchClient) {
this.emit('end');
}
});
}
}
describe('annotationService', () => { describe('annotationsService', () => {
let fakeStore;
let fakeApi; let fakeApi;
let fakeAnnotationMapper;
let fakeStreamer;
let fakeStreamFilter;
let fakeMetadata; let fakeMetadata;
let fakeUris; let fakeStore;
let fakeGroupId;
let fakeDefaultPermissions; let fakeDefaultPermissions;
let fakePrivatePermissions; let fakePrivatePermissions;
let fakeSharedPermissions; let fakeSharedPermissions;
beforeEach(() => { let svc;
sinon.stub(console, 'error');
searchClients = [];
longRunningSearchClient = false;
fakeAnnotationMapper = {
loadAnnotations: sinon.stub(),
unloadAnnotations: sinon.stub(),
};
beforeEach(() => {
fakeApi = { fakeApi = {
annotation: { annotation: {
create: sinon.stub().resolves(fixtures.defaultAnnotation()), create: sinon.stub().resolves(fixtures.defaultAnnotation()),
update: sinon.stub().resolves(fixtures.defaultAnnotation()), update: sinon.stub().resolves(fixtures.defaultAnnotation()),
}, },
search: sinon.stub(),
}; };
fakeDefaultPermissions = sinon.stub(); fakeDefaultPermissions = sinon.stub();
fakePrivatePermissions = sinon.stub();
fakePrivatePermissions = sinon.stub().returns({ fakeSharedPermissions = sinon.stub();
read: ['acct:foo@bar.com'],
update: ['acct:foo@bar.com'],
delete: ['acct:foo@bar.com'],
});
fakeSharedPermissions = sinon.stub().returns({
read: ['group:__world__'],
});
fakeMetadata = { fakeMetadata = {
isAnnotation: sinon.stub(), isAnnotation: sinon.stub(),
...@@ -86,38 +36,18 @@ describe('annotationService', () => { ...@@ -86,38 +36,18 @@ describe('annotationService', () => {
fakeStore = { fakeStore = {
addAnnotations: sinon.stub(), addAnnotations: sinon.stub(),
annotationFetchFinished: sinon.stub(),
annotationFetchStarted: sinon.stub(),
createDraft: sinon.stub(), createDraft: sinon.stub(),
deleteNewAndEmptyDrafts: sinon.stub(), deleteNewAndEmptyDrafts: sinon.stub(),
focusedGroupId: sinon.stub(), focusedGroupId: sinon.stub(),
frames: sinon.stub(),
getDefault: sinon.stub(), getDefault: sinon.stub(),
getDraft: sinon.stub().returns(null), getDraft: sinon.stub().returns(null),
profile: sinon.stub().returns({}), profile: sinon.stub().returns({}),
removeDraft: sinon.stub(), removeDraft: sinon.stub(),
savedAnnotations: sinon.stub(),
selectTab: sinon.stub(), selectTab: sinon.stub(),
setCollapsed: sinon.stub(), setCollapsed: sinon.stub(),
updateFrameAnnotationFetchStatus: sinon.stub(),
};
fakeStreamer = {
setConfig: sinon.stub(),
connect: sinon.stub(),
reconnect: sinon.stub(),
};
fakeStreamFilter = {
resetFilter: sinon.stub().returns({
addClause: sinon.stub(),
}),
getFilter: sinon.stub().returns({}),
}; };
fakeUris = ['http://example.com'];
fakeGroupId = 'group-id';
$imports.$mock({ $imports.$mock({
'../search-client': FakeSearchClient,
'../util/annotation-metadata': fakeMetadata, '../util/annotation-metadata': fakeMetadata,
'../util/permissions': { '../util/permissions': {
defaultPermissions: fakeDefaultPermissions, defaultPermissions: fakeDefaultPermissions,
...@@ -125,31 +55,16 @@ describe('annotationService', () => { ...@@ -125,31 +55,16 @@ describe('annotationService', () => {
sharedPermissions: fakeSharedPermissions, sharedPermissions: fakeSharedPermissions,
}, },
}); });
svc = annotationsService(fakeApi, fakeStore);
}); });
afterEach(() => { afterEach(() => {
console.error.restore();
$imports.$restore(); $imports.$restore();
}); });
function service() {
fakeStore.frames.returns(
fakeUris.map(uri => {
return { uri: uri };
})
);
return annotationsService(
fakeAnnotationMapper,
fakeApi,
fakeStore,
fakeStreamer,
fakeStreamFilter
);
}
describe('create', () => { describe('create', () => {
let now; let now;
let svc;
const getLastAddedAnnotation = () => { const getLastAddedAnnotation = () => {
if (fakeStore.addAnnotations.callCount <= 0) { if (fakeStore.addAnnotations.callCount <= 0) {
...@@ -161,7 +76,6 @@ describe('annotationService', () => { ...@@ -161,7 +76,6 @@ describe('annotationService', () => {
beforeEach(() => { beforeEach(() => {
now = new Date(); now = new Date();
svc = service();
fakeStore.focusedGroupId.returns('mygroup'); fakeStore.focusedGroupId.returns('mygroup');
fakeStore.profile.returns({ fakeStore.profile.returns({
...@@ -307,180 +221,7 @@ describe('annotationService', () => { ...@@ -307,180 +221,7 @@ describe('annotationService', () => {
}); });
}); });
describe('load', () => {
it('unloads any existing annotations', () => {
// When new clients connect, all existing annotations should be unloaded
// before reloading annotations for each currently-connected client.
fakeStore.savedAnnotations.returns([
{ id: fakeUris[0] + '123' },
{ id: fakeUris[0] + '456' },
]);
const svc = service();
svc.load(fakeUris, fakeGroupId);
assert.calledWith(fakeAnnotationMapper.unloadAnnotations, [
sinon.match({ id: fakeUris[0] + '123' }),
sinon.match({ id: fakeUris[0] + '456' }),
]);
});
it('loads all annotations for a URI', () => {
const svc = service();
svc.load(fakeUris, fakeGroupId);
assert.calledWith(fakeAnnotationMapper.loadAnnotations, [
sinon.match({ id: fakeUris[0] + '123' }),
]);
assert.calledWith(fakeAnnotationMapper.loadAnnotations, [
sinon.match({ id: fakeUris[0] + '456' }),
]);
});
it('loads all annotations for a frame with multiple URIs', () => {
const uri = 'http://example.com/test.pdf';
const fingerprint = 'urn:x-pdf:fingerprint';
fakeUris = [uri, fingerprint];
const svc = service();
// Override the default frames set by the service call above.
fakeStore.frames.returns([
{
uri: uri,
metadata: {
documentFingerprint: 'fingerprint',
link: [
{
href: fingerprint,
},
{
href: uri,
},
],
},
},
]);
svc.load(fakeUris, fakeGroupId);
assert.calledWith(fakeAnnotationMapper.loadAnnotations, [
sinon.match({ id: uri + '123' }),
]);
assert.calledWith(fakeAnnotationMapper.loadAnnotations, [
sinon.match({ id: fingerprint + '123' }),
]);
assert.calledWith(fakeAnnotationMapper.loadAnnotations, [
sinon.match({ id: uri + '456' }),
]);
assert.calledWith(fakeAnnotationMapper.loadAnnotations, [
sinon.match({ id: fingerprint + '456' }),
]);
});
it('loads all annotations for all URIs', () => {
fakeUris = ['http://example.com', 'http://foobar.com'];
const svc = service();
svc.load(fakeUris, fakeGroupId);
[
fakeUris[0] + '123',
fakeUris[0] + '456',
fakeUris[1] + '123',
fakeUris[1] + '456',
].forEach(uri => {
assert.calledWith(fakeAnnotationMapper.loadAnnotations, [
sinon.match({ id: uri }),
]);
});
});
it('updates annotation fetch status for all frames', () => {
fakeUris = ['http://example.com', 'http://foobar.com'];
const svc = service();
svc.load(fakeUris, fakeGroupId);
assert.calledWith(
fakeStore.updateFrameAnnotationFetchStatus,
fakeUris[0],
true
);
assert.calledWith(
fakeStore.updateFrameAnnotationFetchStatus,
fakeUris[1],
true
);
});
it('fetches annotations for the specified group', () => {
const svc = service();
svc.load(fakeUris, fakeGroupId);
assert.calledWith(searchClients[0].get, {
uri: fakeUris,
group: fakeGroupId,
});
});
it('loads annotations in batches', () => {
const svc = service();
svc.load(fakeUris, fakeGroupId);
assert.ok(searchClients[0].incremental);
});
it("cancels previously search client if it's still running", () => {
const svc = service();
// Issue a long running load annotations request.
longRunningSearchClient = true;
svc.load(fakeUris, fakeGroupId);
// Issue another load annotations request while the
// previous annotation load is still running.
svc.load(fakeUris, fakeGroupId);
assert.calledOnce(searchClients[0].cancel);
});
it('does not load annotations if URIs list is empty', () => {
fakeUris = [];
const svc = service();
svc.load(fakeUris, fakeGroupId);
assert.notCalled(fakeAnnotationMapper.loadAnnotations);
});
it('calls annotationFetchStarted when it starts searching for annotations', () => {
const svc = service();
svc.load(fakeUris, fakeGroupId);
assert.calledOnce(fakeStore.annotationFetchStarted);
});
it('calls annotationFetchFinished when all annotations have been found', () => {
const svc = service();
svc.load(fakeUris, fakeGroupId);
assert.calledOnce(fakeStore.annotationFetchFinished);
});
it('logs an error to the console if the search client runs into an error', () => {
const svc = service();
const error = new Error('search for annotations failed');
svc.load(fakeUris, fakeGroupId);
searchClients[0].emit('error', error);
assert.calledWith(console.error, error);
});
});
describe('reply', () => { describe('reply', () => {
let svc;
beforeEach(() => {
svc = service();
});
const filledAnnotation = () => { const filledAnnotation = () => {
const annot = fixtures.defaultAnnotation(); const annot = fixtures.defaultAnnotation();
annot.group = 'mix3boop'; annot.group = 'mix3boop';
...@@ -539,12 +280,6 @@ describe('annotationService', () => { ...@@ -539,12 +280,6 @@ describe('annotationService', () => {
}); });
describe('save', () => { describe('save', () => {
let svc;
beforeEach(() => {
svc = service();
});
it('calls the `create` API service for new annotations', () => { it('calls the `create` API service for new annotations', () => {
fakeMetadata.isNew.returns(true); fakeMetadata.isNew.returns(true);
// Using the new-annotation fixture has no bearing on which API method // Using the new-annotation fixture has no bearing on which API method
......
import EventEmitter from 'tiny-emitter';
import loadAnnotationsService, { $imports } from '../load-annotations';
let searchClients;
let longRunningSearchClient = false;
class FakeSearchClient extends EventEmitter {
constructor(searchFn, opts) {
super();
assert.ok(searchFn);
searchClients.push(this);
this.cancel = sinon.stub();
this.incremental = !!opts.incremental;
this.get = sinon.spy(query => {
assert.ok(query.uri);
for (let i = 0; i < query.uri.length; i++) {
const uri = query.uri[i];
this.emit('results', [{ id: uri + '123', group: '__world__' }]);
this.emit('results', [{ id: uri + '456', group: 'private-group' }]);
}
if (!longRunningSearchClient) {
this.emit('end');
}
});
}
}
describe('loadAnnotationsService', () => {
let fakeAnnotationMapper;
let fakeApi;
let fakeStore;
let fakeStreamer;
let fakeStreamFilter;
const fakeGroupId = 'group-id';
let fakeUris;
beforeEach(() => {
sinon.stub(console, 'error');
searchClients = [];
longRunningSearchClient = false;
fakeAnnotationMapper = {
loadAnnotations: sinon.stub(),
unloadAnnotations: sinon.stub(),
};
fakeApi = {
search: sinon.stub(),
};
fakeStore = {
annotationFetchFinished: sinon.stub(),
annotationFetchStarted: sinon.stub(),
frames: sinon.stub(),
savedAnnotations: sinon.stub(),
updateFrameAnnotationFetchStatus: sinon.stub(),
};
fakeStreamer = {
setConfig: sinon.stub(),
connect: sinon.stub(),
reconnect: sinon.stub(),
};
fakeStreamFilter = {
resetFilter: sinon.stub().returns({
addClause: sinon.stub(),
}),
getFilter: sinon.stub().returns({}),
};
fakeUris = ['http://example.com'];
$imports.$mock({
'../search-client': FakeSearchClient,
});
});
afterEach(() => {
console.error.restore();
$imports.$restore();
});
function createService() {
fakeStore.frames.returns(
fakeUris.map(uri => {
return { uri: uri };
})
);
return loadAnnotationsService(
fakeAnnotationMapper,
fakeApi,
fakeStore,
fakeStreamer,
fakeStreamFilter
);
}
describe('load', () => {
it('unloads any existing annotations', () => {
// When new clients connect, all existing annotations should be unloaded
// before reloading annotations for each currently-connected client.
fakeStore.savedAnnotations.returns([
{ id: fakeUris[0] + '123' },
{ id: fakeUris[0] + '456' },
]);
const svc = createService();
svc.load(fakeUris, fakeGroupId);
assert.calledWith(fakeAnnotationMapper.unloadAnnotations, [
sinon.match({ id: fakeUris[0] + '123' }),
sinon.match({ id: fakeUris[0] + '456' }),
]);
});
it('loads all annotations for a URI', () => {
const svc = createService();
svc.load(fakeUris, fakeGroupId);
assert.calledWith(fakeAnnotationMapper.loadAnnotations, [
sinon.match({ id: fakeUris[0] + '123' }),
]);
assert.calledWith(fakeAnnotationMapper.loadAnnotations, [
sinon.match({ id: fakeUris[0] + '456' }),
]);
});
it('loads all annotations for a frame with multiple URIs', () => {
const uri = 'http://example.com/test.pdf';
const fingerprint = 'urn:x-pdf:fingerprint';
fakeUris = [uri, fingerprint];
const svc = createService();
// Override the default frames set by the service call above.
fakeStore.frames.returns([
{
uri: uri,
metadata: {
documentFingerprint: 'fingerprint',
link: [
{
href: fingerprint,
},
{
href: uri,
},
],
},
},
]);
svc.load(fakeUris, fakeGroupId);
assert.calledWith(fakeAnnotationMapper.loadAnnotations, [
sinon.match({ id: uri + '123' }),
]);
assert.calledWith(fakeAnnotationMapper.loadAnnotations, [
sinon.match({ id: fingerprint + '123' }),
]);
assert.calledWith(fakeAnnotationMapper.loadAnnotations, [
sinon.match({ id: uri + '456' }),
]);
assert.calledWith(fakeAnnotationMapper.loadAnnotations, [
sinon.match({ id: fingerprint + '456' }),
]);
});
it('loads all annotations for all URIs', () => {
fakeUris = ['http://example.com', 'http://foobar.com'];
const svc = createService();
svc.load(fakeUris, fakeGroupId);
[
fakeUris[0] + '123',
fakeUris[0] + '456',
fakeUris[1] + '123',
fakeUris[1] + '456',
].forEach(uri => {
assert.calledWith(fakeAnnotationMapper.loadAnnotations, [
sinon.match({ id: uri }),
]);
});
});
it('updates annotation fetch status for all frames', () => {
fakeUris = ['http://example.com', 'http://foobar.com'];
const svc = createService();
svc.load(fakeUris, fakeGroupId);
assert.calledWith(
fakeStore.updateFrameAnnotationFetchStatus,
fakeUris[0],
true
);
assert.calledWith(
fakeStore.updateFrameAnnotationFetchStatus,
fakeUris[1],
true
);
});
it('fetches annotations for the specified group', () => {
const svc = createService();
svc.load(fakeUris, fakeGroupId);
assert.calledWith(searchClients[0].get, {
uri: fakeUris,
group: fakeGroupId,
});
});
it('loads annotations in batches', () => {
const svc = createService();
svc.load(fakeUris, fakeGroupId);
assert.ok(searchClients[0].incremental);
});
it("cancels previously search client if it's still running", () => {
const svc = createService();
// Issue a long running load annotations request.
longRunningSearchClient = true;
svc.load(fakeUris, fakeGroupId);
// Issue another load annotations request while the
// previous annotation load is still running.
svc.load(fakeUris, fakeGroupId);
assert.calledOnce(searchClients[0].cancel);
});
it('does not load annotations if URIs list is empty', () => {
fakeUris = [];
const svc = createService();
svc.load(fakeUris, fakeGroupId);
assert.notCalled(fakeAnnotationMapper.loadAnnotations);
});
it('calls annotationFetchStarted when it starts searching for annotations', () => {
const svc = createService();
svc.load(fakeUris, fakeGroupId);
assert.calledOnce(fakeStore.annotationFetchStarted);
});
it('calls annotationFetchFinished when all annotations have been found', () => {
const svc = createService();
svc.load(fakeUris, fakeGroupId);
assert.calledOnce(fakeStore.annotationFetchFinished);
});
it('logs an error to the console if the search client runs into an error', () => {
const svc = createService();
const error = new Error('search for annotations failed');
svc.load(fakeUris, fakeGroupId);
searchClients[0].emit('error', error);
assert.calledWith(console.error, error);
});
});
});
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