Commit 30f534ad authored by Lyza Danger Gardner's avatar Lyza Danger Gardner

Add `loadThread` method to `loadAnnotationsService`

parent 737f919e
...@@ -3,6 +3,8 @@ ...@@ -3,6 +3,8 @@
*/ */
import SearchClient from '../search-client'; import SearchClient from '../search-client';
import { isReply } from '../util/annotation-metadata';
// @ngInject // @ngInject
export default function loadAnnotationsService( export default function loadAnnotationsService(
api, api,
...@@ -61,7 +63,56 @@ export default function loadAnnotationsService( ...@@ -61,7 +63,56 @@ export default function loadAnnotationsService(
searchClient.get({ uri: uris, group: groupId }); searchClient.get({ uri: uris, group: groupId });
} }
/**
* Fetch all annotations in the same thread as `id` and add them to the store.
*
* @param {string} id - Annotation ID. This may be an annotation or a reply.
* @return Promise<Annotation[]> - The annotation, followed by any replies.
*/
async function loadThread(id) {
let annotation;
let replySearchResult;
// Clear out any annotations already in the store before fetching new ones
store.clearAnnotations();
try {
store.annotationFetchStarted();
// 1. Fetch the annotation indicated by `id` — the target annotation
annotation = await api.annotation.get({ id });
// 2. If annotation is not the top-level annotation in its thread,
// fetch the top-level annotation
if (isReply(annotation)) {
annotation = await api.annotation.get({ id: annotation.references[0] });
}
// 3. Fetch all of the annotations in the thread, based on the
// top-level annotation
replySearchResult = await api.search({ references: annotation.id });
} finally {
store.annotationFetchFinished();
}
const threadAnnotations = [annotation, ...replySearchResult.rows];
store.addAnnotations(threadAnnotations);
// If we've been successful in retrieving a thread, with a top-level annotation,
// configure the connection to the real-time update service to send us
// updates to any of the annotations in the thread.
if (!isReply(annotation)) {
streamFilter
.addClause('/references', 'one_of', annotation.id, true)
.addClause('/id', 'equals', annotation.id, true);
streamer.setConfig('filter', { filter: streamFilter.getFilter() });
streamer.connect();
}
return threadAnnotations;
}
return { return {
load, load,
loadThread,
}; };
} }
...@@ -43,13 +43,17 @@ describe('loadAnnotationsService', () => { ...@@ -43,13 +43,17 @@ describe('loadAnnotationsService', () => {
longRunningSearchClient = false; longRunningSearchClient = false;
fakeApi = { fakeApi = {
search: sinon.stub(), search: sinon.stub().returns({ rows: [] }),
annotation: {
get: sinon.stub(),
},
}; };
fakeStore = { fakeStore = {
addAnnotations: sinon.stub(), addAnnotations: sinon.stub(),
annotationFetchFinished: sinon.stub(), annotationFetchFinished: sinon.stub(),
annotationFetchStarted: sinon.stub(), annotationFetchStarted: sinon.stub(),
clearAnnotations: sinon.stub(),
frames: sinon.stub(), frames: sinon.stub(),
removeAnnotations: sinon.stub(), removeAnnotations: sinon.stub(),
savedAnnotations: sinon.stub(), savedAnnotations: sinon.stub(),
...@@ -62,6 +66,9 @@ describe('loadAnnotationsService', () => { ...@@ -62,6 +66,9 @@ describe('loadAnnotationsService', () => {
reconnect: sinon.stub(), reconnect: sinon.stub(),
}; };
fakeStreamFilter = { fakeStreamFilter = {
addClause: sinon.stub().returns({
addClause: sinon.stub(),
}),
resetFilter: sinon.stub().returns({ resetFilter: sinon.stub().returns({
addClause: sinon.stub(), addClause: sinon.stub(),
}), }),
...@@ -257,4 +264,187 @@ describe('loadAnnotationsService', () => { ...@@ -257,4 +264,187 @@ describe('loadAnnotationsService', () => {
assert.calledWith(console.error, error); assert.calledWith(console.error, error);
}); });
}); });
describe('loadThread', () => {
let threadAnnotations = [
{ id: 'parent_annotation_1' },
{ id: 'parent_annotation_2', references: ['parent_annotation_1'] },
{
id: 'target_annotation',
references: ['parent_annotation_1', 'parent_annotation_2'],
},
];
it('clears annotations from the store first', () => {
const svc = createService();
svc.loadThread('target_annotation');
assert.calledOnce(fakeStore.clearAnnotations);
});
describe('fetching the target annotation', () => {
beforeEach(() => {
fakeApi.annotation.get.onFirstCall().resolves({
id: 'target_annotation',
references: [],
});
});
it('fetches annotation with given `id`', async () => {
const svc = createService();
await svc.loadThread('target_annotation');
assert.calledWith(
fakeApi.annotation.get,
sinon.match({ id: 'target_annotation' })
);
});
it('records the start and end of annotation fetch with the store', async () => {
const svc = createService();
await svc.loadThread('target_annotation');
assert.calledOnce(fakeStore.annotationFetchStarted);
assert.calledOnce(fakeStore.annotationFetchFinished);
});
it('stops the annotation fetch with the store on error', async () => {
fakeApi.annotation.get.onFirstCall().throws();
const svc = createService();
try {
await svc.loadThread('target_annotation');
} catch (e) {
assert.calledOnce(fakeStore.annotationFetchStarted);
assert.calledOnce(fakeStore.annotationFetchFinished);
}
});
});
describe('fetching top-level annotation in thread', () => {
beforeEach(() => {
fakeApi.annotation.get.onFirstCall().resolves({
id: 'target_annotation',
references: ['parent_annotation_1', 'parent_annotation_2'],
});
fakeApi.annotation.get.onSecondCall().resolves({
id: 'parent_annotation_1',
references: [],
});
});
it('fetches top-level annotation', async () => {
const svc = createService();
await svc.loadThread('target_annotation');
assert.calledWith(
fakeApi.annotation.get,
sinon.match({ id: 'parent_annotation_1' })
);
});
});
describe('fetching other annotations in the thread', () => {
beforeEach(() => {
fakeApi.annotation.get.onFirstCall().resolves({
id: 'target_annotation',
references: ['parent_annotation_1', 'parent_annotation_2'],
});
fakeApi.annotation.get.onSecondCall().resolves({
id: 'parent_annotation_1',
references: [],
});
fakeApi.search.resolves({
rows: [threadAnnotations[1], threadAnnotations[2]],
});
});
it('retrieves all annotations in the thread', async () => {
const svc = createService();
await svc.loadThread('target_annotation');
assert.calledWith(
fakeApi.search,
sinon.match({ references: 'parent_annotation_1' })
);
});
it('adds all of the annotations in the thread to the store', async () => {
const svc = createService();
await svc.loadThread('target_annotation');
assert.calledWith(fakeStore.addAnnotations, sinon.match.array);
});
it('returns thread annotations', async () => {
const svc = createService();
const annots = await svc.loadThread('target_annotation');
assert.equal(annots[0].id, 'parent_annotation_1');
assert.equal(annots[1].id, 'parent_annotation_2');
assert.equal(annots[2].id, 'target_annotation');
});
});
describe('connecting to streamer for thread updates', () => {
beforeEach(() => {
fakeApi.annotation.get.onFirstCall().resolves({
id: 'target_annotation',
references: ['parent_annotation_1', 'parent_annotation_2'],
});
fakeApi.annotation.get.onSecondCall().resolves({
id: 'parent_annotation_1',
references: [],
});
fakeApi.search.resolves({
rows: [threadAnnotations[1], threadAnnotations[2]],
});
});
it('does not connect to the streamer if no top-level annotation available', async () => {
// Make it so the "top-level" annotation isn't really top level: it has references
// and so is a reply
fakeApi.annotation.get.onSecondCall().resolves({
id: 'parent_annotation_1',
references: ['something_else'],
});
const svc = createService();
svc.loadThread('target_annotation');
await new Promise(resolve => setTimeout(resolve, 0));
assert.notCalled(fakeStreamer.connect);
});
it('configures the stream filter for changes to the thread', async () => {
const fakeAddClause = sinon.stub();
fakeStreamFilter.addClause.returns({ addClause: fakeAddClause });
fakeStreamFilter.getFilter.returns('filter');
const svc = createService();
svc.loadThread('target_annotation');
await new Promise(resolve => setTimeout(resolve, 0));
assert.calledWith(
fakeStreamFilter.addClause,
'/references',
'one_of',
'parent_annotation_1',
true
);
assert.calledWith(
fakeAddClause,
'/id',
'equals',
'parent_annotation_1',
true
);
assert.calledWith(
fakeStreamer.setConfig,
'filter',
sinon.match({ filter: 'filter' })
);
assert.calledOnce(fakeStreamer.connect);
});
});
});
}); });
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