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
64cb045a
Unverified
Commit
64cb045a
authored
Jul 19, 2019
by
Hannah Stepanek
Committed by
GitHub
Jul 19, 2019
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1211 from hypothesis/move-load-anns-to-service
Move load annotations to service
parents
11ab71ea
3c9115cd
Changes
5
Hide whitespace changes
Inline
Side-by-side
Showing
5 changed files
with
402 additions
and
434 deletions
+402
-434
sidebar-content.js
src/sidebar/components/sidebar-content.js
+19
-130
sidebar-content-test.js
src/sidebar/components/test/sidebar-content-test.js
+51
-304
index.js
src/sidebar/index.js
+1
-0
annotations.js
src/sidebar/services/annotations.js
+63
-0
annotations-test.js
src/sidebar/services/test/annotations-test.js
+268
-0
No files found.
src/sidebar/components/sidebar-content.js
View file @
64cb045a
'use strict'
;
const
SearchClient
=
require
(
'../search-client'
);
const
events
=
require
(
'../events'
);
const
isThirdPartyService
=
require
(
'../util/is-third-party-service'
);
const
tabs
=
require
(
'../tabs'
);
/**
* Returns the group ID of the first annotation in `results` whose
* ID is `annId`.
*/
function
getGroupID
(
annId
,
results
)
{
const
annot
=
results
.
find
(
function
(
annot
)
{
return
annot
.
id
===
annId
;
});
if
(
!
annot
)
{
return
null
;
}
return
annot
.
group
;
}
// @ngInject
function
SidebarContentController
(
$scope
,
analytics
,
annotations
,
store
,
annotationMapper
,
api
,
features
,
frameSync
,
groups
,
rootThread
,
settings
,
streamer
,
streamFilter
streamer
)
{
const
self
=
this
;
...
...
@@ -90,61 +71,7 @@ function SidebarContentController(
}
}
const
searchClients
=
[];
function
_resetAnnotations
()
{
annotationMapper
.
unloadAnnotations
(
store
.
savedAnnotations
());
}
function
_loadAnnotationsFor
(
uris
,
group
)
{
const
searchClient
=
new
SearchClient
(
api
.
search
,
{
// If no group is specified, we are fetching annotations from
// all groups in order to find out which group contains the selected
// annotation, therefore we need to load all chunks before processing
// the results
incremental
:
!!
group
,
});
searchClients
.
push
(
searchClient
);
searchClient
.
on
(
'results'
,
function
(
results
)
{
if
(
store
.
hasSelectedAnnotations
())
{
// Focus the group containing the selected annotation and filter
// annotations to those from this group
let
groupID
=
getGroupID
(
store
.
getFirstSelectedAnnotationId
(),
results
);
if
(
!
groupID
)
{
// If the selected annotation is not available, fall back to
// loading annotations for the currently focused group
groupID
=
groups
.
focused
().
id
;
}
results
=
results
.
filter
(
function
(
result
)
{
return
result
.
group
===
groupID
;
});
groups
.
focus
(
groupID
);
}
if
(
results
.
length
)
{
annotationMapper
.
loadAnnotations
(
results
);
}
});
searchClient
.
on
(
'end'
,
function
()
{
// Remove client from list of active search clients.
//
// $evalAsync is required here because search results are emitted
// asynchronously. A better solution would be that the loading state is
// tracked as part of the app state.
$scope
.
$evalAsync
(
function
()
{
searchClients
.
splice
(
searchClients
.
indexOf
(
searchClient
),
1
);
});
store
.
frames
().
forEach
(
function
(
frame
)
{
if
(
0
<=
uris
.
indexOf
(
frame
.
uri
))
{
store
.
updateFrameAnnotationFetchStatus
(
frame
.
uri
,
true
);
}
});
});
searchClient
.
get
({
uri
:
uris
,
group
:
group
});
}
this
.
isLoading
=
function
()
{
this
.
isLoading
=
()
=>
{
if
(
!
store
.
frames
().
some
(
function
(
frame
)
{
return
frame
.
uri
;
...
...
@@ -153,47 +80,9 @@ function SidebarContentController(
// The document's URL isn't known so the document must still be loading.
return
true
;
}
if
(
searchClients
.
length
>
0
)
{
// We're still waiting for annotation search results from the API.
return
true
;
}
return
false
;
return
store
.
isFetchingAnnotations
();
};
/**
* Load annotations for all URLs associated with `frames`.
*/
function
loadAnnotations
()
{
_resetAnnotations
();
searchClients
.
forEach
(
function
(
client
)
{
client
.
cancel
();
});
// If there is no selection, load annotations only for the focused group.
//
// If there is a selection, we load annotations for all groups, find out
// which group the first selected annotation is in and then filter the
// results on the client by that group.
//
// In the common case where the total number of annotations on
// a page that are visible to the user is not greater than
// the batch size, this saves an extra roundtrip to the server
// to fetch the selected annotation in order to determine which group
// it is in before fetching the remaining annotations.
const
group
=
store
.
hasSelectedAnnotations
()
?
null
:
groups
.
focused
().
id
;
const
searchUris
=
store
.
searchUris
();
if
(
searchUris
.
length
>
0
)
{
_loadAnnotationsFor
(
searchUris
,
group
);
streamFilter
.
resetFilter
().
addClause
(
'/uri'
,
'one_of'
,
searchUris
);
streamer
.
setConfig
(
'filter'
,
{
filter
:
streamFilter
.
getFilter
()
});
}
}
$scope
.
$on
(
'sidebarOpened'
,
function
()
{
analytics
.
track
(
analytics
.
events
.
SIDEBAR_OPENED
);
...
...
@@ -233,27 +122,25 @@ function SidebarContentController(
// Re-fetch annotations when focused group, logged-in user or connected frames
// change.
$scope
.
$watch
(
()
=>
[
groups
.
focused
(),
store
.
profile
().
userid
,
...
store
.
searchUris
()],
([
currentGroup
],
[
prevGroup
])
=>
{
if
(
!
currentGroup
)
{
// When switching accounts, groups are cleared and so the focused group
()
=>
[
store
.
focusedGroupId
(),
store
.
profile
().
userid
,
...
store
.
searchUris
(),
],
([
currentGroupId
],
[
prevGroupId
])
=>
{
if
(
!
currentGroupId
)
{
// When switching accounts, groups are cleared and so the focused group id
// will be null for a brief period of time.
store
.
clearSelectedAnnotations
();
return
;
}
if
(
!
prevGroup
||
currentGroup
.
id
!==
prevGroup
.
id
)
{
// The focused group may be changed during loading annotations as a result
// of switching to the group containing a direct-linked annotation.
//
// In that case, we don't want to trigger reloading annotations again.
if
(
this
.
isLoading
())
{
return
;
}
if
(
!
prevGroupId
||
currentGroupId
!==
prevGroupId
)
{
store
.
clearSelectedAnnotations
();
}
loadAnnotations
();
const
searchUris
=
store
.
searchUris
();
annotations
.
load
(
searchUris
,
currentGroupId
);
},
true
);
...
...
@@ -277,7 +164,7 @@ function SidebarContentController(
this
.
scrollTo
=
scrollToAnnotation
;
this
.
selectedGroupUnavailable
=
function
()
{
return
!
this
.
isLoading
()
&&
store
.
getState
().
directLinkedGroupFetchFailed
;
return
store
.
getState
().
directLinkedGroupFetchFailed
;
};
this
.
selectedAnnotationUnavailable
=
function
()
{
...
...
@@ -310,7 +197,9 @@ function SidebarContentController(
// selection is available to the user, show the CTA.
const
selectedID
=
store
.
getFirstSelectedAnnotationId
();
return
(
!
this
.
isLoading
()
&&
!!
selectedID
&&
store
.
annotationExists
(
selectedID
)
!
store
.
isFetchingAnnotations
()
&&
!!
selectedID
&&
store
.
annotationExists
(
selectedID
)
);
};
}
...
...
src/sidebar/components/test/sidebar-content-test.js
View file @
64cb045a
...
...
@@ -5,32 +5,6 @@ const EventEmitter = require('tiny-emitter');
const
events
=
require
(
'../../events'
);
const
sidebarContent
=
require
(
'../sidebar-content'
);
const
util
=
require
(
'../../directive/test/util'
);
let
searchClients
;
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
(
function
(
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'
}]);
}
this
.
emit
(
'end'
);
});
}
}
class
FakeRootThread
extends
EventEmitter
{
constructor
()
{
...
...
@@ -47,16 +21,11 @@ describe('sidebar.components.sidebar-content', function() {
let
store
;
let
ctrl
;
let
fakeAnalytics
;
let
fakeAnnotationMapper
;
let
fakeDrafts
;
let
fakeFeatures
;
let
fakeAnnotations
;
let
fakeFrameSync
;
let
fakeGroups
;
let
fakeRootThread
;
let
fakeSettings
;
let
fakeApi
;
let
fakeStreamer
;
let
fakeStreamFilter
;
let
sandbox
;
before
(
function
()
{
...
...
@@ -70,7 +39,6 @@ describe('sidebar.components.sidebar-content', function() {
beforeEach
(()
=>
{
angular
.
mock
.
module
(
function
(
$provide
)
{
searchClients
=
[];
sandbox
=
sinon
.
sandbox
.
create
();
fakeAnalytics
=
{
...
...
@@ -78,65 +46,32 @@ describe('sidebar.components.sidebar-content', function() {
events
:
{},
};
fakeAnnotationMapper
=
{
loadAnnotations
:
sandbox
.
stub
(),
unloadAnnotations
:
sandbox
.
stub
(),
};
fakeFrameSync
=
{
focusAnnotations
:
sinon
.
stub
(),
scrollToAnnotation
:
sinon
.
stub
(),
};
fakeDrafts
=
{
unsaved
:
sandbox
.
stub
().
returns
([]),
};
fakeFeatures
=
{
flagEnabled
:
sandbox
.
stub
().
returns
(
true
),
};
fakeStreamer
=
{
setConfig
:
sandbox
.
stub
(),
connect
:
sandbox
.
stub
(),
reconnect
:
sandbox
.
stub
(),
};
fakeStreamFilter
=
{
resetFilter
:
sandbox
.
stub
().
returnsThis
(),
addClause
:
sandbox
.
stub
().
returnsThis
(),
getFilter
:
sandbox
.
stub
().
returns
({}),
};
fakeGroups
=
{
focused
:
sinon
.
stub
().
returns
({
id
:
'foo'
}),
focus
:
sinon
.
stub
(),
fakeAnnotations
=
{
load
:
sinon
.
stub
(),
};
fakeRootThread
=
new
FakeRootThread
();
fakeSettings
=
{};
fakeApi
=
{
search
:
sinon
.
stub
(),
};
$provide
.
value
(
'analytics'
,
fakeAnalytics
);
$provide
.
value
(
'annotationMapper'
,
fakeAnnotationMapper
);
$provide
.
value
(
'api'
,
fakeApi
);
$provide
.
value
(
'drafts'
,
fakeDrafts
);
$provide
.
value
(
'features'
,
fakeFeatures
);
$provide
.
value
(
'frameSync'
,
fakeFrameSync
);
$provide
.
value
(
'rootThread'
,
fakeRootThread
);
$provide
.
value
(
'streamer'
,
fakeStreamer
);
$provide
.
value
(
'streamFilter'
,
fakeStreamFilter
);
$provide
.
value
(
'groups'
,
fakeGroups
);
$provide
.
value
(
'annotations'
,
fakeAnnotations
);
$provide
.
value
(
'settings'
,
fakeSettings
);
});
sidebarContent
.
$imports
.
$mock
({
'../search-client'
:
FakeSearchClient
,
});
});
afterEach
(()
=>
{
...
...
@@ -149,25 +84,17 @@ describe('sidebar.components.sidebar-content', function() {
});
}
function
createSidebarContent
(
{
userid
}
=
{
userid
:
'acct:person@example.com'
}
)
{
return
util
.
createDirective
(
document
,
'sidebarContent'
,
{
auth
:
{
status
:
userid
?
'logged-in'
:
'logged-out'
,
userid
:
userid
,
},
search
:
sinon
.
stub
().
returns
({
query
:
sinon
.
stub
()
}),
onLogin
:
sinon
.
stub
(),
});
}
const
makeSidebarContentController
=
()
=>
{
angular
.
mock
.
inject
(
function
(
$componentController
,
_store_
,
_$rootScope_
)
{
$rootScope
=
_$rootScope_
;
$scope
=
$rootScope
.
$new
();
store
=
_store_
;
store
.
updateFrameAnnotationFetchStatus
=
sinon
.
stub
();
store
.
clearGroups
();
store
.
loadGroups
([{
id
:
'group-id'
}]);
store
.
focusGroup
(
'group-id'
);
ctrl
=
$componentController
(
'sidebarContent'
,
{
$scope
:
$scope
},
...
...
@@ -186,6 +113,23 @@ describe('sidebar.components.sidebar-content', function() {
return
sandbox
.
restore
();
});
describe
(
'isLoading'
,
()
=>
{
it
(
"returns true if the document's url isn't known"
,
()
=>
{
assert
.
isTrue
(
ctrl
.
isLoading
());
});
it
(
'returns true if annotations are still being fetched'
,
()
=>
{
setFrames
([{
uri
:
'http://www.example.com'
}]);
store
.
annotationFetchStarted
(
'tag:foo'
);
assert
.
isTrue
(
ctrl
.
isLoading
());
});
it
(
'returns false if annotations have been fetched'
,
()
=>
{
setFrames
([{
uri
:
'http://www.example.com'
}]);
assert
.
isFalse
(
ctrl
.
isLoading
());
});
});
describe
(
'showSelectedTabs'
,
()
=>
{
beforeEach
(()
=>
{
setFrames
([{
uri
:
'http://www.example.com'
}]);
...
...
@@ -214,211 +158,10 @@ describe('sidebar.components.sidebar-content', function() {
});
});
describe
(
'#loadAnnotations'
,
function
()
{
it
(
'unloads any existing annotations'
,
function
()
{
// When new clients connect, all existing annotations should be unloaded
// before reloading annotations for each currently-connected client
store
.
addAnnotations
([{
id
:
'123'
}]);
const
uri1
=
'http://example.com/page-a'
;
let
frames
=
[{
uri
:
uri1
}];
setFrames
(
frames
);
$scope
.
$digest
();
fakeAnnotationMapper
.
unloadAnnotations
=
sandbox
.
spy
();
const
uri2
=
'http://example.com/page-b'
;
frames
=
frames
.
concat
({
uri
:
uri2
});
setFrames
(
frames
);
$scope
.
$digest
();
assert
.
calledWith
(
fakeAnnotationMapper
.
unloadAnnotations
,
store
.
getState
().
annotations
);
});
it
(
'loads all annotations for a frame'
,
function
()
{
const
uri
=
'http://example.com'
;
setFrames
([{
uri
:
uri
}]);
$scope
.
$digest
();
const
loadSpy
=
fakeAnnotationMapper
.
loadAnnotations
;
assert
.
calledWith
(
loadSpy
,
[
sinon
.
match
({
id
:
uri
+
'123'
})]);
assert
.
calledWith
(
loadSpy
,
[
sinon
.
match
({
id
:
uri
+
'456'
})]);
});
it
(
'loads all annotations for a frame with multiple urls'
,
function
()
{
const
uri
=
'http://example.com/test.pdf'
;
const
fingerprint
=
'urn:x-pdf:fingerprint'
;
setFrames
([
{
uri
:
uri
,
metadata
:
{
documentFingerprint
:
'fingerprint'
,
link
:
[
{
href
:
fingerprint
,
},
{
href
:
uri
,
},
],
},
},
]);
$scope
.
$digest
();
const
loadSpy
=
fakeAnnotationMapper
.
loadAnnotations
;
assert
.
calledWith
(
loadSpy
,
[
sinon
.
match
({
id
:
uri
+
'123'
})]);
assert
.
calledWith
(
loadSpy
,
[
sinon
.
match
({
id
:
fingerprint
+
'123'
})]);
assert
.
calledWith
(
loadSpy
,
[
sinon
.
match
({
id
:
uri
+
'456'
})]);
assert
.
calledWith
(
loadSpy
,
[
sinon
.
match
({
id
:
fingerprint
+
'456'
})]);
});
it
(
'loads all annotations for all frames'
,
function
()
{
const
uris
=
[
'http://example.com'
,
'http://foobar.com'
];
setFrames
(
uris
.
map
(
function
(
uri
)
{
return
{
uri
:
uri
};
})
);
$scope
.
$digest
();
const
loadSpy
=
fakeAnnotationMapper
.
loadAnnotations
;
assert
.
calledWith
(
loadSpy
,
[
sinon
.
match
({
id
:
uris
[
0
]
+
'123'
})]);
assert
.
calledWith
(
loadSpy
,
[
sinon
.
match
({
id
:
uris
[
0
]
+
'456'
})]);
assert
.
calledWith
(
loadSpy
,
[
sinon
.
match
({
id
:
uris
[
1
]
+
'123'
})]);
assert
.
calledWith
(
loadSpy
,
[
sinon
.
match
({
id
:
uris
[
1
]
+
'456'
})]);
});
it
(
'updates annotation fetch status for all frames'
,
function
()
{
const
frameUris
=
[
'http://example.com'
,
'http://foobar.com'
];
setFrames
(
frameUris
.
map
(
function
(
frameUri
)
{
return
{
uri
:
frameUri
};
})
);
$scope
.
$digest
();
const
updateSpy
=
store
.
updateFrameAnnotationFetchStatus
;
assert
.
isTrue
(
updateSpy
.
calledWith
(
frameUris
[
0
],
true
));
assert
.
isTrue
(
updateSpy
.
calledWith
(
frameUris
[
1
],
true
));
});
context
(
'when there is a direct-linked group error'
,
()
=>
{
beforeEach
(()
=>
{
setFrames
([{
uri
:
'http://www.example.com'
}]);
fakeSettings
.
group
=
'group-id'
;
store
.
setDirectLinkedGroupFetchFailed
();
$scope
.
$digest
();
});
[
null
,
'acct:person@example.com'
].
forEach
(
userid
=>
{
it
(
'displays same group error message regardless of login state'
,
()
=>
{
const
element
=
createSidebarContent
({
userid
});
const
sidebarContentError
=
element
.
find
(
'.sidebar-content-error'
);
const
errorMessage
=
sidebarContentError
.
attr
(
'logged-in-error-message'
);
assert
.
equal
(
errorMessage
,
"'This group is not available.'"
);
});
});
it
(
'selectedGroupUnavailable returns true'
,
()
=>
{
assert
.
isTrue
(
ctrl
.
selectedGroupUnavailable
());
});
});
context
(
'when there is a direct-linked group selection'
,
()
=>
{
beforeEach
(()
=>
{
setFrames
([{
uri
:
'http://www.example.com'
}]);
fakeSettings
.
group
=
'group-id'
;
store
.
loadGroups
([{
id
:
fakeSettings
.
group
}]);
store
.
focusGroup
(
fakeSettings
.
group
);
fakeGroups
.
focused
.
returns
({
id
:
fakeSettings
.
group
});
$scope
.
$digest
();
});
it
(
'selectedGroupUnavailable returns false'
,
()
=>
{
assert
.
isFalse
(
ctrl
.
selectedGroupUnavailable
());
});
it
(
'fetches annotations for the direct-linked group'
,
()
=>
{
assert
.
calledWith
(
searchClients
[
0
].
get
,
{
uri
:
[
'http://www.example.com'
],
group
:
'group-id'
,
});
});
});
context
(
'when there is a direct-linked annotation selection'
,
function
()
{
const
uri
=
'http://example.com'
;
const
id
=
uri
+
'123'
;
beforeEach
(
function
()
{
setFrames
([{
uri
:
uri
}]);
store
.
selectAnnotations
([
id
]);
$scope
.
$digest
();
});
it
(
"switches to the selected annotation's group"
,
function
()
{
assert
.
calledWith
(
fakeGroups
.
focus
,
'__world__'
);
assert
.
calledOnce
(
fakeAnnotationMapper
.
loadAnnotations
);
assert
.
calledWith
(
fakeAnnotationMapper
.
loadAnnotations
,
[
{
id
:
uri
+
'123'
,
group
:
'__world__'
},
]);
});
it
(
'fetches annotations for all groups'
,
function
()
{
assert
.
calledWith
(
searchClients
[
0
].
get
,
{
uri
:
[
uri
],
group
:
null
});
});
it
(
'loads annotations in one batch'
,
function
()
{
assert
.
notOk
(
searchClients
[
0
].
incremental
);
});
});
context
(
'when there is no selection'
,
function
()
{
const
uri
=
'http://example.com'
;
beforeEach
(
function
()
{
setFrames
([{
uri
:
uri
}]);
fakeGroups
.
focused
.
returns
({
id
:
'a-group'
});
$scope
.
$digest
();
});
it
(
'fetches annotations for the current group'
,
function
()
{
assert
.
calledWith
(
searchClients
[
0
].
get
,
{
uri
:
[
uri
],
group
:
'a-group'
,
});
});
it
(
'loads annotations in batches'
,
function
()
{
assert
.
ok
(
searchClients
[
0
].
incremental
);
});
});
context
(
'when the selected annotation is not available'
,
function
()
{
const
uri
=
'http://example.com'
;
const
id
=
uri
+
'does-not-exist'
;
beforeEach
(
function
()
{
setFrames
([{
uri
:
uri
}]);
store
.
selectAnnotations
([
id
]);
fakeGroups
.
focused
.
returns
({
id
:
'private-group'
});
$scope
.
$digest
();
});
it
(
'loads annotations from the focused group instead'
,
function
()
{
assert
.
calledWith
(
fakeGroups
.
focus
,
'private-group'
);
assert
.
calledWith
(
fakeAnnotationMapper
.
loadAnnotations
,
[
{
group
:
'private-group'
,
id
:
'http://example.com456'
},
]);
});
});
});
function
connectFrameAndPerformInitialFetch
()
{
setFrames
([{
uri
:
'https://a-page.com'
}]);
$scope
.
$digest
();
fakeAnnotation
Mapper
.
loadAnnotations
.
reset
();
fakeAnnotation
s
.
load
.
reset
();
}
context
(
'when the search URIs of connected frames change'
,
()
=>
{
...
...
@@ -429,7 +172,11 @@ describe('sidebar.components.sidebar-content', function() {
$scope
.
$digest
();
assert
.
called
(
fakeAnnotationMapper
.
loadAnnotations
);
assert
.
calledWith
(
fakeAnnotations
.
load
,
[
'https://a-page.com'
,
'https://new-frame.com'
],
'group-id'
);
});
});
...
...
@@ -444,7 +191,11 @@ describe('sidebar.components.sidebar-content', function() {
store
.
updateSession
(
newProfile
);
$scope
.
$digest
();
assert
.
called
(
fakeAnnotationMapper
.
loadAnnotations
);
assert
.
calledWith
(
fakeAnnotations
.
load
,
[
'https://a-page.com'
],
'group-id'
);
});
it
(
'does not reload annotations if the user ID is the same'
,
()
=>
{
...
...
@@ -457,7 +208,7 @@ describe('sidebar.components.sidebar-content', function() {
store
.
updateSession
(
newProfile
);
$scope
.
$digest
();
assert
.
notCalled
(
fakeAnnotation
Mapper
.
loadAnnotations
);
assert
.
notCalled
(
fakeAnnotation
s
.
load
);
});
});
...
...
@@ -486,33 +237,30 @@ describe('sidebar.components.sidebar-content', function() {
// annotations loaded.
store
.
addAnnotations
([{
id
:
'123'
}]);
store
.
addAnnotations
=
sinon
.
stub
();
fakeDrafts
.
unsaved
.
returns
([{
id
:
uri
+
'123'
},
{
id
:
uri
+
'456'
}]);
setFrames
([{
uri
:
uri
}]);
$scope
.
$digest
();
fakeAnnotations
.
load
=
sinon
.
stub
();
});
function
changeGroup
()
{
fakeGroups
.
focused
.
returns
({
id
:
'different-group'
});
$scope
.
$digest
();
}
it
(
'should load annotations for the new group'
,
()
=>
{
const
loadSpy
=
fakeAnnotationMapper
.
loadAnnotations
;
changeGroup
();
store
.
loadGroups
([{
id
:
'different-group'
}]);
store
.
focusGroup
(
'different-group'
);
assert
.
calledWith
(
fakeAnnotationMapper
.
unloadAnnotations
,
[
sinon
.
match
({
id
:
'123'
}),
]);
$scope
.
$digest
();
assert
.
calledWith
(
loadSpy
,
[
sinon
.
match
({
id
:
uri
+
'123'
})]);
assert
.
calledWith
(
loadSpy
,
[
sinon
.
match
({
id
:
uri
+
'456'
})]);
assert
.
calledWith
(
fakeAnnotations
.
load
,
[
'http://example.com'
],
'different-group'
);
});
it
(
'should clear the selection'
,
()
=>
{
store
.
selectAnnotations
([
'123'
]);
store
.
loadGroups
([{
id
:
'different-group'
}]);
store
.
focusGroup
(
'different-group'
);
changeGroup
();
$scope
.
$digest
();
assert
.
isFalse
(
store
.
hasSelectedAnnotations
());
});
...
...
@@ -562,10 +310,9 @@ describe('sidebar.components.sidebar-content', function() {
});
it
(
"doesn't show a message if the document isn't loaded yet"
,
function
()
{
// No search requests have been sent yet.
searchClients
=
[];
// There is a selection but the selected annotation isn't available.
store
.
selectAnnotations
([
'missing'
]);
store
.
annotationFetchStarted
();
$scope
.
$digest
();
assert
.
isFalse
(
ctrl
.
selectedAnnotationUnavailable
());
...
...
src/sidebar/index.js
View file @
64cb045a
...
...
@@ -207,6 +207,7 @@ function startAngularApp(config) {
.
service
(
'analytics'
,
require
(
'./services/analytics'
))
.
service
(
'annotationMapper'
,
require
(
'./services/annotation-mapper'
))
.
service
(
'annotations'
,
require
(
'./services/annotations'
))
.
service
(
'api'
,
require
(
'./services/api'
))
.
service
(
'apiRoutes'
,
require
(
'./services/api-routes'
))
.
service
(
'auth'
,
require
(
'./services/oauth-auth'
))
...
...
src/sidebar/services/annotations.js
0 → 100644
View file @
64cb045a
'use strict'
;
const
SearchClient
=
require
(
'../search-client'
);
// @ngInject
function
annotations
(
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
,
};
}
module
.
exports
=
annotations
;
src/sidebar/services/test/annotations-test.js
0 → 100644
View file @
64cb045a
'use strict'
;
const
annotations
=
require
(
'../annotations'
);
const
EventEmitter
=
require
(
'tiny-emitter'
);
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
(
'annotations'
,
()
=>
{
let
fakeStore
;
let
fakeApi
;
let
fakeAnnotationMapper
;
let
fakeStreamer
;
let
fakeStreamFilter
;
let
fakeUris
;
let
fakeGroupId
;
beforeEach
(()
=>
{
sinon
.
stub
(
console
,
'error'
);
searchClients
=
[];
longRunningSearchClient
=
false
;
fakeAnnotationMapper
=
{
loadAnnotations
:
sinon
.
stub
(),
unloadAnnotations
:
sinon
.
stub
(),
};
fakeApi
=
{
search
:
sinon
.
stub
(),
};
fakeStore
=
{
getState
:
sinon
.
stub
(),
frames
:
sinon
.
stub
(),
searchUris
:
sinon
.
stub
(),
savedAnnotations
:
sinon
.
stub
(),
hasSelectedAnnotations
:
sinon
.
stub
(),
updateFrameAnnotationFetchStatus
:
sinon
.
stub
(),
annotationFetchStarted
:
sinon
.
stub
(),
annotationFetchFinished
:
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'
;
annotations
.
$imports
.
$mock
({
'../search-client'
:
FakeSearchClient
,
});
});
afterEach
(()
=>
{
console
.
error
.
restore
();
annotations
.
$imports
.
$restore
();
});
function
service
()
{
fakeStore
.
frames
.
returns
(
fakeUris
.
map
(
uri
=>
{
return
{
uri
:
uri
};
})
);
return
annotations
(
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
=
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
);
});
});
});
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