Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
C
coopwire-hypothesis
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
孙灵跃 Leon Sun
coopwire-hypothesis
Commits
81871feb
Unverified
Commit
81871feb
authored
May 28, 2020
by
Lyza Gardner
Committed by
GitHub
May 28, 2020
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #2209 from hypothesis/fix-single-annotation-load
Rectify infinite load on single-annotation, stream views
parents
737f919e
8bfec058
Changes
6
Hide whitespace changes
Inline
Side-by-side
Showing
6 changed files
with
333 additions
and
166 deletions
+333
-166
annotation-viewer-content.js
src/sidebar/components/annotation-viewer-content.js
+7
-53
stream-content.js
src/sidebar/components/stream-content.js
+20
-3
annotation-viewer-content-test.js
...sidebar/components/test/annotation-viewer-content-test.js
+47
-109
stream-content-test.js
src/sidebar/components/test/stream-content-test.js
+17
-0
load-annotations.js
src/sidebar/services/load-annotations.js
+51
-0
load-annotations-test.js
src/sidebar/services/test/load-annotations-test.js
+191
-1
No files found.
src/sidebar/components/annotation-viewer-content.js
View file @
81871feb
...
@@ -12,13 +12,10 @@ import SidebarContentError from './sidebar-content-error';
...
@@ -12,13 +12,10 @@ import SidebarContentError from './sidebar-content-error';
* The main content for the single annotation page (aka. https://hypothes.is/a/<annotation ID>)
* The main content for the single annotation page (aka. https://hypothes.is/a/<annotation ID>)
*/
*/
function
AnnotationViewerContent
({
function
AnnotationViewerContent
({
api
,
loadAnnotationsService
,
onLogin
,
onLogin
,
rootThread
:
rootThreadService
,
rootThread
:
rootThreadService
,
streamer
,
streamFilter
,
})
{
})
{
const
addAnnotations
=
useStore
(
store
=>
store
.
addAnnotations
);
const
annotationId
=
useStore
(
store
=>
store
.
routeParams
().
id
);
const
annotationId
=
useStore
(
store
=>
store
.
routeParams
().
id
);
const
clearAnnotations
=
useStore
(
store
=>
store
.
clearAnnotations
);
const
clearAnnotations
=
useStore
(
store
=>
store
.
clearAnnotations
);
const
highlightAnnotations
=
useStore
(
store
=>
store
.
highlightAnnotations
);
const
highlightAnnotations
=
useStore
(
store
=>
store
.
highlightAnnotations
);
...
@@ -34,13 +31,12 @@ function AnnotationViewerContent({
...
@@ -34,13 +31,12 @@ function AnnotationViewerContent({
setFetchError
(
false
);
setFetchError
(
false
);
clearAnnotations
();
clearAnnotations
();
fetchThread
(
api
,
annotationId
)
loadAnnotationsService
.
loadThread
(
annotationId
)
.
then
(
annots
=>
{
.
then
(
annots
=>
{
addAnnotations
(
annots
);
// Find the top-level annotation in the thread that `annotationId` is
// Find the top-level annotation in the thread that `annotationId` is
// part of. This will be different to `annotationId` if `annotationId`
// part of. This will be different to `annotationId` if `annotationId`
// is a reply.
// is a reply.
A top-level annotation will not have any references.
const
topLevelAnnot
=
annots
.
filter
(
const
topLevelAnnot
=
annots
.
filter
(
ann
=>
(
ann
.
references
||
[]).
length
===
0
ann
=>
(
ann
.
references
||
[]).
length
===
0
)[
0
];
)[
0
];
...
@@ -59,14 +55,6 @@ function AnnotationViewerContent({
...
@@ -59,14 +55,6 @@ function AnnotationViewerContent({
return
;
return
;
}
}
// Configure the connection to the real-time update service to send us
// updates to any of the annotations in the thread.
streamFilter
.
addClause
(
'/references'
,
'one_of'
,
topLevelAnnot
.
id
,
true
)
.
addClause
(
'/id'
,
'equals'
,
topLevelAnnot
.
id
,
true
);
streamer
.
setConfig
(
'filter'
,
{
filter
:
streamFilter
.
getFilter
()
});
streamer
.
connect
();
// Make the full thread of annotations visible. By default replies are
// Make the full thread of annotations visible. By default replies are
// not shown until the user expands the thread.
// not shown until the user expands the thread.
annots
.
forEach
(
annot
=>
setCollapsed
(
annot
.
id
,
false
));
annots
.
forEach
(
annot
=>
setCollapsed
(
annot
.
id
,
false
));
...
@@ -88,13 +76,10 @@ function AnnotationViewerContent({
...
@@ -88,13 +76,10 @@ function AnnotationViewerContent({
userid
,
userid
,
// Static dependencies.
// Static dependencies.
addAnnotations
,
api
,
clearAnnotations
,
clearAnnotations
,
highlightAnnotations
,
highlightAnnotations
,
loadAnnotationsService
,
setCollapsed
,
setCollapsed
,
streamFilter
,
streamer
,
]);
]);
return
(
return
(
...
@@ -114,44 +99,13 @@ AnnotationViewerContent.propTypes = {
...
@@ -114,44 +99,13 @@ AnnotationViewerContent.propTypes = {
onLogin
:
propTypes
.
func
.
isRequired
,
onLogin
:
propTypes
.
func
.
isRequired
,
// Injected.
// Injected.
api
:
propTypes
.
object
,
loadAnnotationsService
:
propTypes
.
object
,
rootThread
:
propTypes
.
object
,
rootThread
:
propTypes
.
object
,
streamer
:
propTypes
.
object
,
streamFilter
:
propTypes
.
object
,
};
};
AnnotationViewerContent
.
injectedProps
=
[
AnnotationViewerContent
.
injectedProps
=
[
'
api
'
,
'
loadAnnotationsService
'
,
'rootThread'
,
'rootThread'
,
'streamer'
,
'streamFilter'
,
];
];
// NOTE: The function below is intentionally at the bottom of the file.
//
// Putting it at the top resulted in an issue where the `createElement` import
// wasn't correctly referenced in the body of `AnnotationViewerContent` in
// the compiled JS, causing a runtime error.
/**
* Fetch all annotations in the same thread as `id`.
*
* @param {Object} api - API client
* @param {string} id - Annotation ID. This may be an annotation or a reply.
* @return Promise<Annotation[]> - The annotation, followed by any replies.
*/
async
function
fetchThread
(
api
,
id
)
{
let
annot
=
await
api
.
annotation
.
get
({
id
});
if
(
annot
.
references
&&
annot
.
references
.
length
)
{
// This is a reply, fetch the top-level annotation
annot
=
await
api
.
annotation
.
get
({
id
:
annot
.
references
[
0
]
});
}
// Fetch all replies to the top-level annotation.
const
replySearchResult
=
await
api
.
search
({
references
:
annot
.
id
});
return
[
annot
,
...
replySearchResult
.
rows
];
}
export
default
withServices
(
AnnotationViewerContent
);
export
default
withServices
(
AnnotationViewerContent
);
src/sidebar/components/stream-content.js
View file @
81871feb
...
@@ -17,6 +17,12 @@ function StreamContent({
...
@@ -17,6 +17,12 @@ function StreamContent({
toastMessenger
,
toastMessenger
,
})
{
})
{
const
addAnnotations
=
useStore
(
store
=>
store
.
addAnnotations
);
const
addAnnotations
=
useStore
(
store
=>
store
.
addAnnotations
);
const
annotationFetchStarted
=
useStore
(
store
=>
store
.
annotationFetchStarted
);
const
annotationFetchFinished
=
useStore
(
store
=>
store
.
annotationFetchFinished
);
const
clearAnnotations
=
useStore
(
store
=>
store
.
clearAnnotations
);
const
clearAnnotations
=
useStore
(
store
=>
store
.
clearAnnotations
);
const
currentQuery
=
useStore
(
store
=>
store
.
routeParams
().
q
);
const
currentQuery
=
useStore
(
store
=>
store
.
routeParams
().
q
);
const
setSortKey
=
useStore
(
store
=>
store
.
setSortKey
);
const
setSortKey
=
useStore
(
store
=>
store
.
setSortKey
);
...
@@ -38,10 +44,21 @@ function StreamContent({
...
@@ -38,10 +44,21 @@ function StreamContent({
...
searchFilter
.
toObject
(
query
),
...
searchFilter
.
toObject
(
query
),
};
};
const
results
=
await
api
.
search
(
queryParams
);
try
{
addAnnotations
([...
results
.
rows
,
...
results
.
replies
]);
annotationFetchStarted
();
const
results
=
await
api
.
search
(
queryParams
);
addAnnotations
([...
results
.
rows
,
...
results
.
replies
]);
}
finally
{
annotationFetchFinished
();
}
},
},
[
addAnnotations
,
api
,
searchFilter
]
[
addAnnotations
,
annotationFetchStarted
,
annotationFetchFinished
,
api
,
searchFilter
,
]
);
);
// Update the stream when this route is initially displayed and whenever
// Update the stream when this route is initially displayed and whenever
...
...
src/sidebar/components/test/annotation-viewer-content-test.js
View file @
81871feb
...
@@ -8,38 +8,14 @@ import AnnotationViewerContent, {
...
@@ -8,38 +8,14 @@ import AnnotationViewerContent, {
$imports
,
$imports
,
}
from
'../annotation-viewer-content'
;
}
from
'../annotation-viewer-content'
;
/**
* Fake implementation of the `api` service.
*/
class
FakeApi
{
constructor
(
annots
)
{
this
.
annotations
=
annots
;
this
.
annotation
=
{
get
:
async
query
=>
this
.
annotations
.
find
(
a
=>
a
.
id
===
query
.
id
),
};
}
async
search
(
query
)
{
let
matches
=
[];
if
(
query
.
references
)
{
matches
=
this
.
annotations
.
filter
(
a
=>
a
.
references
&&
a
.
references
.
includes
(
query
.
references
)
);
}
return
{
rows
:
matches
};
}
}
describe
(
'AnnotationViewerContent'
,
()
=>
{
describe
(
'AnnotationViewerContent'
,
()
=>
{
let
fakeStore
;
let
fakeStore
;
let
fakeOnLogin
;
let
fakeRootThread
;
let
fakeRootThread
;
let
fakeStreamer
;
let
fakeLoadAnnotationsService
;
let
fakeStreamFilter
;
beforeEach
(()
=>
{
beforeEach
(()
=>
{
fakeStore
=
{
fakeStore
=
{
addAnnotations
:
sinon
.
stub
(),
clearAnnotations
:
sinon
.
stub
(),
clearAnnotations
:
sinon
.
stub
(),
getState
:
sinon
.
stub
().
returns
({}),
getState
:
sinon
.
stub
().
returns
({}),
highlightAnnotations
:
sinon
.
stub
(),
highlightAnnotations
:
sinon
.
stub
(),
...
@@ -48,21 +24,13 @@ describe('AnnotationViewerContent', () => {
...
@@ -48,21 +24,13 @@ describe('AnnotationViewerContent', () => {
setCollapsed
:
sinon
.
stub
(),
setCollapsed
:
sinon
.
stub
(),
};
};
fakeRootThread
=
{
thread
:
sinon
.
stub
().
returns
({})
};
fakeLoadAnnotationsService
=
{
loadThread
:
sinon
.
stub
().
resolves
([]),
fakeStreamer
=
{
setConfig
:
()
=>
{},
connect
:
()
=>
{},
};
};
fakeStreamFilter
=
{
fakeOnLogin
=
sinon
.
stub
();
addClause
:
()
=>
{
return
{
fakeRootThread
=
{
thread
:
sinon
.
stub
().
returns
({})
};
addClause
:
()
=>
{},
};
},
getFilter
:
()
=>
{},
};
$imports
.
$mock
(
mockImportedComponents
());
$imports
.
$mock
(
mockImportedComponents
());
$imports
.
$mock
({
$imports
.
$mock
({
...
@@ -74,55 +42,54 @@ describe('AnnotationViewerContent', () => {
...
@@ -74,55 +42,54 @@ describe('AnnotationViewerContent', () => {
$imports
.
$restore
();
$imports
.
$restore
();
});
});
function
createComponent
(
{
api
,
onLogin
=
sinon
.
stub
()
})
{
function
createComponent
(
props
=
{
})
{
return
mount
(
return
mount
(
<
AnnotationViewerContent
<
AnnotationViewerContent
api
=
{
api
}
loadAnnotationsService
=
{
fakeLoadAnnotationsService
}
onLogin
=
{
o
nLogin
}
onLogin
=
{
fakeO
nLogin
}
rootThread
=
{
fakeRootThread
}
rootThread
=
{
fakeRootThread
}
streamer
=
{
fakeStreamer
}
{...
props
}
streamFilter
=
{
fakeStreamFilter
}
/
>
/
>
);
);
}
}
function
waitForAnnotationsToLoad
()
{
return
waitFor
(()
=>
fakeStore
.
addAnnotations
.
called
);
}
describe
(
'the standalone view for a top-level annotation'
,
()
=>
{
describe
(
'the standalone view for a top-level annotation'
,
()
=>
{
it
(
'loads the annotation and all replies'
,
async
()
=>
{
it
(
'loads the annotation thread'
,
()
=>
{
const
fakeApi
=
new
FakeApi
([
createComponent
();
{
id
:
'test_annotation_id'
},
{
id
:
'test_reply_id'
,
references
:
[
'test_annotation_id'
]
},
]);
createComponent
({
api
:
fakeApi
});
await
waitForAnnotationsToLoad
();
assert
.
calledOnce
(
fakeLoadAnnotationsService
.
loadThread
);
assert
.
calledOnce
(
fakeStore
.
addAnnotations
);
assert
.
calledWith
(
fakeStore
.
addAnnotations
,
sinon
.
match
(
fakeApi
.
annotations
)
);
});
});
it
(
'does not highlight any annotations'
,
async
()
=>
{
context
(
'successfully-loaded annotation thread'
,
()
=>
{
const
fakeApi
=
new
FakeApi
([
beforeEach
(()
=>
{
{
id
:
'test_annotation_id'
},
fakeLoadAnnotationsService
.
loadThread
.
resolves
([
{
id
:
'test_reply_id'
,
references
:
[
'test_annotation_id'
]
},
{
id
:
'test_annotation_id'
},
]);
{
id
:
'test_reply_id'
,
references
:
[
'test_annotation_id'
]
},
createComponent
({
api
:
fakeApi
});
]);
});
await
waitForAnnotationsToLoad
();
it
(
'does not highlight any annotations'
,
async
()
=>
{
createComponent
();
await
new
Promise
(
resolve
=>
setTimeout
(
resolve
,
0
));
assert
.
notCalled
(
fakeStore
.
highlightAnnotations
);
});
assert
.
notCalled
(
fakeStore
.
highlightAnnotations
);
it
(
'expands the thread'
,
async
()
=>
{
createComponent
();
await
new
Promise
(
resolve
=>
setTimeout
(
resolve
,
0
));
assert
.
calledWith
(
fakeStore
.
setCollapsed
,
'test_annotation_id'
,
false
);
assert
.
calledWith
(
fakeStore
.
setCollapsed
,
'test_reply_id'
,
false
);
});
});
});
it
(
'shows an error if the annotation could not be fetched'
,
async
()
=>
{
it
(
'shows an error if the annotation could not be fetched'
,
async
()
=>
{
const
fakeApi
=
new
FakeApi
([]
);
fakeLoadAnnotationsService
.
loadThread
.
rejects
(
);
const
onLogin
=
sinon
.
stub
();
const
onLogin
=
sinon
.
stub
();
const
wrapper
=
createComponent
({
api
:
fakeApi
,
onLogin
});
const
wrapper
=
createComponent
({
onLogin
});
// Initially the annotation is not available to the user, so an error
// Initially the annotation is not available to the user, so an error
// should be shown.
// should be shown.
...
@@ -138,57 +105,28 @@ describe('AnnotationViewerContent', () => {
...
@@ -138,57 +105,28 @@ describe('AnnotationViewerContent', () => {
onLoginRequest
();
onLoginRequest
();
assert
.
called
(
onLogin
);
assert
.
called
(
onLogin
);
// After the user logs in, the annotation should be shown.
fakeLoadAnnotationsService
.
loadThread
.
resetHistory
();
fakeApi
.
annotations
=
[{
id
:
'test_annotation_id'
}];
fakeLoadAnnotationsService
.
loadThread
.
resolves
([
{
id
:
'test_annotation_id'
},
]);
fakeStore
.
profile
.
returns
({
userid
:
'acct:jimsmith@hypothes.is'
});
fakeStore
.
profile
.
returns
({
userid
:
'acct:jimsmith@hypothes.is'
});
// Force re-render. `useStore` would do this in the actual app.
// Force re-render. `useStore` would do this in the actual app.
wrapper
.
setProps
({});
wrapper
.
setProps
({});
await
waitFor
AnnotationsToLoad
(
);
await
waitFor
(()
=>
fakeLoadAnnotationsService
.
loadThread
.
called
);
assert
.
isFalse
(
wrapper
.
exists
(
'SidebarContentError'
));
assert
.
isFalse
(
wrapper
.
exists
(
'SidebarContentError'
));
});
});
});
describe
(
'the standalone view for a reply'
,
()
=>
{
it
(
'loads the top-level annotation and all replies'
,
async
()
=>
{
const
fakeApi
=
new
FakeApi
([
{
id
:
'parent_id'
},
{
id
:
'test_annotation_id'
,
references
:
[
'parent_id'
]
},
]);
createComponent
({
api
:
fakeApi
});
await
waitForAnnotationsToLoad
();
assert
.
calledWith
(
fakeStore
.
addAnnotations
,
sinon
.
match
(
fakeApi
.
annotations
)
);
});
it
(
'expands the thread'
,
async
()
=>
{
const
fakeApi
=
new
FakeApi
([
{
id
:
'parent_id'
},
{
id
:
'test_annotation_id'
,
references
:
[
'parent_id'
]
},
]);
createComponent
({
api
:
fakeApi
});
await
waitForAnnotationsToLoad
();
assert
.
calledWith
(
fakeStore
.
setCollapsed
,
'parent_id'
,
false
);
assert
.
calledWith
(
fakeStore
.
setCollapsed
,
'test_annotation_id'
,
false
);
});
it
(
'highlights the reply'
,
async
()
=>
{
it
(
'highlights the
annotation if it is a
reply'
,
async
()
=>
{
const
fakeApi
=
new
FakeApi
([
fakeLoadAnnotationsService
.
loadThread
.
resolves
([
{
id
:
'parent_id'
},
{
id
:
'parent_id'
},
{
id
:
'test_annotation_id'
,
references
:
[
'parent_id'
]
},
{
id
:
'test_annotation_id'
,
references
:
[
'parent_id'
]
},
]);
]);
createComponent
({
api
:
fakeApi
});
await
waitForAnnotationsToLoad
();
createComponent
();
await
new
Promise
(
resolve
=>
setTimeout
(
resolve
,
0
));
assert
.
calledWith
(
assert
.
calledWith
(
fakeStore
.
highlightAnnotations
,
fakeStore
.
highlightAnnotations
,
sinon
.
match
([
'test_annotation_id'
])
sinon
.
match
([
'test_annotation_id'
])
...
...
src/sidebar/components/test/stream-content-test.js
View file @
81871feb
...
@@ -28,6 +28,8 @@ describe('StreamContent', () => {
...
@@ -28,6 +28,8 @@ describe('StreamContent', () => {
fakeStore
=
{
fakeStore
=
{
addAnnotations
:
sinon
.
stub
(),
addAnnotations
:
sinon
.
stub
(),
annotationFetchStarted
:
sinon
.
stub
(),
annotationFetchFinished
:
sinon
.
stub
(),
clearAnnotations
:
sinon
.
spy
(),
clearAnnotations
:
sinon
.
spy
(),
getState
:
sinon
.
stub
().
returns
({}),
getState
:
sinon
.
stub
().
returns
({}),
routeParams
:
sinon
.
stub
().
returns
({
id
:
'test'
}),
routeParams
:
sinon
.
stub
().
returns
({
id
:
'test'
}),
...
@@ -69,6 +71,21 @@ describe('StreamContent', () => {
...
@@ -69,6 +71,21 @@ describe('StreamContent', () => {
assert
.
equal
(
fakeApi
.
search
.
firstCall
.
args
[
0
].
_separate_replies
,
true
);
assert
.
equal
(
fakeApi
.
search
.
firstCall
.
args
[
0
].
_separate_replies
,
true
);
});
});
it
(
'records the start and finish of the fetch request in the store'
,
async
()
=>
{
createComponent
();
await
waitFor
(()
=>
fakeStore
.
annotationFetchStarted
.
calledOnce
);
await
waitFor
(()
=>
fakeStore
.
annotationFetchFinished
.
calledOnce
);
});
it
(
'records the finish of the fetch request on error'
,
async
()
=>
{
fakeApi
.
search
.
throws
();
createComponent
();
await
waitFor
(()
=>
fakeStore
.
annotationFetchStarted
.
calledOnce
);
await
waitFor
(()
=>
fakeStore
.
annotationFetchFinished
.
calledOnce
);
});
it
(
'loads the annotations and replies into the store'
,
async
()
=>
{
it
(
'loads the annotations and replies into the store'
,
async
()
=>
{
fakeApi
.
search
.
resolves
({
fakeApi
.
search
.
resolves
({
rows
:
[
'annotation_1'
,
'annotation_2'
],
rows
:
[
'annotation_1'
,
'annotation_2'
],
...
...
src/sidebar/services/load-annotations.js
View file @
81871feb
...
@@ -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
,
};
};
}
}
src/sidebar/services/test/load-annotations-test.js
View file @
81871feb
...
@@ -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
);
});
});
});
});
});
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment