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
0a44e65b
Unverified
Commit
0a44e65b
authored
Mar 11, 2020
by
Lyza Gardner
Committed by
GitHub
Mar 11, 2020
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #1884 from hypothesis/fix-change-group-mutation-violation
Fix mutation violation when changing focused group
parents
7314300e
576e810a
Changes
14
Show whitespace changes
Inline
Side-by-side
Showing
14 changed files
with
231 additions
and
194 deletions
+231
-194
group-list-item.js
src/sidebar/components/group-list-item.js
+1
-2
group-list-item-test.js
src/sidebar/components/test/group-list-item-test.js
+2
-2
events.js
src/sidebar/events.js
+0
-3
groups.js
src/sidebar/services/groups.js
+44
-40
persisted-defaults.js
src/sidebar/services/persisted-defaults.js
+1
-0
root-thread.js
src/sidebar/services/root-thread.js
+0
-18
groups-test.js
src/sidebar/services/test/groups-test.js
+101
-81
persisted-defaults-test.js
src/sidebar/services/test/persisted-defaults-test.js
+16
-10
root-thread-test.js
src/sidebar/services/test/root-thread-test.js
+0
-36
annotations.js
src/sidebar/store/modules/annotations.js
+11
-0
defaults.js
src/sidebar/store/modules/defaults.js
+1
-0
annotations-test.js
src/sidebar/store/modules/test/annotations-test.js
+35
-0
defaults-test.js
src/sidebar/store/modules/test/defaults-test.js
+5
-1
annotation-fixtures.js
src/sidebar/test/annotation-fixtures.js
+14
-1
No files found.
src/sidebar/components/group-list-item.js
View file @
0a44e65b
...
@@ -33,14 +33,13 @@ function GroupListItem({
...
@@ -33,14 +33,13 @@ function GroupListItem({
const
actions
=
useStore
(
store
=>
({
const
actions
=
useStore
(
store
=>
({
clearDirectLinkedGroupFetchFailed
:
store
.
clearDirectLinkedGroupFetchFailed
,
clearDirectLinkedGroupFetchFailed
:
store
.
clearDirectLinkedGroupFetchFailed
,
clearDirectLinkedIds
:
store
.
clearDirectLinkedIds
,
clearDirectLinkedIds
:
store
.
clearDirectLinkedIds
,
focusGroup
:
store
.
focusGroup
,
}));
}));
const
focusGroup
=
()
=>
{
const
focusGroup
=
()
=>
{
analytics
.
track
(
analytics
.
events
.
GROUP_SWITCH
);
analytics
.
track
(
analytics
.
events
.
GROUP_SWITCH
);
actions
.
clearDirectLinkedGroupFetchFailed
();
actions
.
clearDirectLinkedGroupFetchFailed
();
actions
.
clearDirectLinkedIds
();
actions
.
clearDirectLinkedIds
();
actions
.
focusGroup
(
group
.
id
);
groupsService
.
focus
(
group
.
id
);
};
};
const
leaveGroup
=
()
=>
{
const
leaveGroup
=
()
=>
{
...
...
src/sidebar/components/test/group-list-item-test.js
View file @
0a44e65b
...
@@ -29,7 +29,6 @@ describe('GroupListItem', () => {
...
@@ -29,7 +29,6 @@ describe('GroupListItem', () => {
};
};
fakeStore
=
{
fakeStore
=
{
focusGroup
:
sinon
.
stub
(),
focusedGroupId
:
sinon
.
stub
().
returns
(
'groupid'
),
focusedGroupId
:
sinon
.
stub
().
returns
(
'groupid'
),
clearDirectLinkedIds
:
sinon
.
stub
(),
clearDirectLinkedIds
:
sinon
.
stub
(),
clearDirectLinkedGroupFetchFailed
:
sinon
.
stub
(),
clearDirectLinkedGroupFetchFailed
:
sinon
.
stub
(),
...
@@ -50,6 +49,7 @@ describe('GroupListItem', () => {
...
@@ -50,6 +49,7 @@ describe('GroupListItem', () => {
};
};
fakeGroupsService
=
{
fakeGroupsService
=
{
focus
:
sinon
.
stub
(),
leave
:
sinon
.
stub
(),
leave
:
sinon
.
stub
(),
};
};
...
@@ -111,7 +111,7 @@ describe('GroupListItem', () => {
...
@@ -111,7 +111,7 @@ describe('GroupListItem', () => {
.
props
()
.
props
()
.
onClick
();
.
onClick
();
assert
.
calledWith
(
fake
Store
.
focusGroup
,
fakeGroup
.
id
);
assert
.
calledWith
(
fake
GroupsService
.
focus
,
fakeGroup
.
id
);
assert
.
calledWith
(
fakeAnalytics
.
track
,
fakeAnalytics
.
events
.
GROUP_SWITCH
);
assert
.
calledWith
(
fakeAnalytics
.
track
,
fakeAnalytics
.
events
.
GROUP_SWITCH
);
});
});
...
...
src/sidebar/events.js
View file @
0a44e65b
...
@@ -18,9 +18,6 @@ export default {
...
@@ -18,9 +18,6 @@ export default {
// UI state changes
// UI state changes
/** The currently selected group changed */
GROUP_FOCUSED
:
'groupFocused'
,
// Annotation events
// Annotation events
/** A new annotation has been created locally. */
/** A new annotation has been created locally. */
...
...
src/sidebar/services/groups.js
View file @
0a44e65b
const
STORAGE_KEY
=
'hypothesis.groups.focus'
;
import
events
from
'../events'
;
import
serviceConfig
from
'../service-config'
;
import
{
isReply
}
from
'../util/annotation-metadata'
;
import
{
combineGroups
}
from
'../util/groups'
;
import
{
awaitStateChange
}
from
'../util/state'
;
const
DEFAULT_ORG_ID
=
'__default__'
;
const
DEFAULT_ORG_ID
=
'__default__'
;
/**
/**
...
@@ -11,18 +16,12 @@ const DEFAULT_ORGANIZATION = {
...
@@ -11,18 +16,12 @@ const DEFAULT_ORGANIZATION = {
encodeURIComponent
(
require
(
'../../images/icons/logo.svg'
)),
encodeURIComponent
(
require
(
'../../images/icons/logo.svg'
)),
};
};
import
events
from
'../events'
;
import
serviceConfig
from
'../service-config'
;
import
{
combineGroups
}
from
'../util/groups'
;
import
{
awaitStateChange
}
from
'../util/state'
;
// @ngInject
// @ngInject
export
default
function
groups
(
export
default
function
groups
(
$rootScope
,
$rootScope
,
store
,
store
,
api
,
api
,
isSidebar
,
isSidebar
,
localStorage
,
serviceUrl
,
serviceUrl
,
session
,
session
,
settings
,
settings
,
...
@@ -42,6 +41,40 @@ export default function groups(
...
@@ -42,6 +41,40 @@ export default function groups(
return
awaitStateChange
(
store
,
mainUri
);
return
awaitStateChange
(
store
,
mainUri
);
}
}
/**
* Update the focused group. Update the store, then check to see if
* there are any new (unsaved) annotations—if so, update those annotations
* such that they are associated with the newly-focused group.
*/
function
focus
(
groupId
)
{
const
prevGroupId
=
store
.
focusedGroupId
();
store
.
focusGroup
(
groupId
);
const
newGroupId
=
store
.
focusedGroupId
();
const
groupHasChanged
=
prevGroupId
!==
newGroupId
&&
prevGroupId
!==
null
;
if
(
groupHasChanged
)
{
// Move any top-level new annotations to the newly-focused group.
// Leave replies where they are.
const
updatedAnnotations
=
[];
store
.
newAnnotations
().
forEach
(
annot
=>
{
if
(
!
isReply
(
annot
))
{
updatedAnnotations
.
push
(
Object
.
assign
({},
annot
,
{
group
:
newGroupId
})
);
}
});
if
(
updatedAnnotations
.
length
)
{
store
.
addAnnotations
(
updatedAnnotations
);
}
// Persist this group as the last focused group default
store
.
setDefault
(
'focusedGroup'
,
newGroupId
);
}
}
/**
/**
* Filter the returned list of groups from the API.
* Filter the returned list of groups from the API.
*
*
...
@@ -276,17 +309,17 @@ export default function groups(
...
@@ -276,17 +309,17 @@ export default function groups(
// Step 5. Load the groups into the store and focus the appropriate
// Step 5. Load the groups into the store and focus the appropriate
// group.
// group.
const
isFirstLoad
=
store
.
allGroups
().
length
===
0
;
const
isFirstLoad
=
store
.
allGroups
().
length
===
0
;
const
prevFocusedGroup
=
localStorage
.
getItem
(
STORAGE_KEY
);
const
prevFocusedGroup
=
store
.
getDefault
(
'focusedGroup'
);
store
.
loadGroups
(
groups
);
store
.
loadGroups
(
groups
);
if
(
isFirstLoad
)
{
if
(
isFirstLoad
)
{
if
(
groups
.
some
(
g
=>
g
.
id
===
directLinkedAnnotationGroupId
))
{
if
(
groups
.
some
(
g
=>
g
.
id
===
directLinkedAnnotationGroupId
))
{
store
.
focusGroup
(
directLinkedAnnotationGroupId
);
focus
(
directLinkedAnnotationGroupId
);
}
else
if
(
groups
.
some
(
g
=>
g
.
id
===
directLinkedGroupId
))
{
}
else
if
(
groups
.
some
(
g
=>
g
.
id
===
directLinkedGroupId
))
{
store
.
focusGroup
(
directLinkedGroupId
);
focus
(
directLinkedGroupId
);
}
else
if
(
groups
.
some
(
g
=>
g
.
id
===
prevFocusedGroup
))
{
}
else
if
(
groups
.
some
(
g
=>
g
.
id
===
prevFocusedGroup
))
{
store
.
focusGroup
(
prevFocusedGroup
);
focus
(
prevFocusedGroup
);
}
}
}
}
...
@@ -316,34 +349,6 @@ export default function groups(
...
@@ -316,34 +349,6 @@ export default function groups(
});
});
}
}
/** Return the currently focused group. If no group is explicitly focused we
* will check localStorage to see if we have persisted a focused group from
* a previous session. Lastly, we fall back to the first group available.
*/
function
focused
()
{
return
store
.
focusedGroup
();
}
/** Set the group with the passed id as the currently focused group. */
function
focus
(
id
)
{
store
.
focusGroup
(
id
);
}
// Persist the focused group to storage when it changes.
let
prevFocusedId
=
store
.
focusedGroupId
();
store
.
subscribe
(()
=>
{
const
focusedId
=
store
.
focusedGroupId
();
// The focused group may be null when the user login state changes.
if
(
focusedId
!==
null
&&
focusedId
!==
prevFocusedId
)
{
prevFocusedId
=
focusedId
;
localStorage
.
setItem
(
STORAGE_KEY
,
focusedId
);
// Emit the `GROUP_FOCUSED` event for code that still relies on it.
$rootScope
.
$broadcast
(
events
.
GROUP_FOCUSED
,
focusedId
);
}
});
// refetch the list of groups when user changes
// refetch the list of groups when user changes
$rootScope
.
$on
(
events
.
USER_CHANGED
,
()
=>
{
$rootScope
.
$on
(
events
.
USER_CHANGED
,
()
=>
{
// FIXME Makes a second api call on page load. better way?
// FIXME Makes a second api call on page load. better way?
...
@@ -368,7 +373,6 @@ export default function groups(
...
@@ -368,7 +373,6 @@ export default function groups(
leave
:
leave
,
leave
:
leave
,
load
:
load
,
load
:
load
,
focused
:
focused
,
focus
:
focus
,
focus
:
focus
,
};
};
}
}
src/sidebar/services/persisted-defaults.js
View file @
0a44e65b
...
@@ -5,6 +5,7 @@
...
@@ -5,6 +5,7 @@
const
DEFAULT_KEYS
=
{
const
DEFAULT_KEYS
=
{
annotationPrivacy
:
'hypothesis.privacy'
,
annotationPrivacy
:
'hypothesis.privacy'
,
focusedGroup
:
'hypothesis.groups.focus'
,
};
};
// @ngInject
// @ngInject
...
...
src/sidebar/services/root-thread.js
View file @
0a44e65b
...
@@ -123,24 +123,6 @@ export default function RootThread(
...
@@ -123,24 +123,6 @@ export default function RootThread(
store
.
removeAnnotations
([
annotation
]);
store
.
removeAnnotations
([
annotation
]);
});
});
// Once the focused group state is moved to the app state store, then the
// logic in this event handler can be moved to the annotations reducer.
$rootScope
.
$on
(
events
.
GROUP_FOCUSED
,
function
(
event
,
focusedGroupId
)
{
const
updatedAnnots
=
store
.
getState
()
.
annotations
.
annotations
.
filter
(
function
(
ann
)
{
return
metadata
.
isNew
(
ann
)
&&
!
metadata
.
isReply
(
ann
);
})
.
map
(
function
(
ann
)
{
return
Object
.
assign
(
ann
,
{
group
:
focusedGroupId
,
});
});
if
(
updatedAnnots
.
length
>
0
)
{
store
.
addAnnotations
(
updatedAnnots
);
}
});
/**
/**
* Build the root conversation thread from the given UI state.
* Build the root conversation thread from the given UI state.
* @return {Thread}
* @return {Thread}
...
...
src/sidebar/services/test/groups-test.js
View file @
0a44e65b
import
events
from
'../../events'
;
import
events
from
'../../events'
;
import
fakeReduxStore
from
'../../test/fake-redux-store'
;
import
fakeReduxStore
from
'../../test/fake-redux-store'
;
import
groups
from
'../groups'
;
import
groups
,
{
$imports
}
from
'../groups'
;
/**
/**
* Generate a truth table containing every possible combination of a set of
* Generate a truth table containing every possible combination of a set of
...
@@ -44,15 +44,19 @@ describe('groups', function() {
...
@@ -44,15 +44,19 @@ describe('groups', function() {
let
fakeSession
;
let
fakeSession
;
let
fakeSettings
;
let
fakeSettings
;
let
fakeApi
;
let
fakeApi
;
let
fakeLocalStorage
;
let
fakeRootScope
;
let
fakeRootScope
;
let
fakeServiceUrl
;
let
fakeServiceUrl
;
let
fakeMetadata
;
beforeEach
(
function
()
{
beforeEach
(
function
()
{
fakeAuth
=
{
fakeAuth
=
{
tokenGetter
:
sinon
.
stub
().
returns
(
'1234'
),
tokenGetter
:
sinon
.
stub
().
returns
(
'1234'
),
};
};
fakeMetadata
=
{
isReply
:
sinon
.
stub
(),
};
fakeStore
=
fakeReduxStore
(
fakeStore
=
fakeReduxStore
(
{
{
frames
:
[{
uri
:
'http://example.org'
}],
frames
:
[{
uri
:
'http://example.org'
}],
...
@@ -66,9 +70,13 @@ describe('groups', function() {
...
@@ -66,9 +70,13 @@ describe('groups', function() {
},
},
},
},
{
{
addAnnotations
:
sinon
.
stub
(),
focusGroup
:
sinon
.
stub
(),
focusGroup
:
sinon
.
stub
(),
focusedGroupId
:
sinon
.
stub
(),
getDefault
:
sinon
.
stub
(),
getGroup
:
sinon
.
stub
(),
getGroup
:
sinon
.
stub
(),
loadGroups
:
sinon
.
stub
(),
loadGroups
:
sinon
.
stub
(),
newAnnotations
:
sinon
.
stub
().
returns
([]),
allGroups
()
{
allGroups
()
{
return
this
.
getState
().
groups
.
groups
;
return
this
.
getState
().
groups
.
groups
;
},
},
...
@@ -78,20 +86,13 @@ describe('groups', function() {
...
@@ -78,20 +86,13 @@ describe('groups', function() {
mainFrame
()
{
mainFrame
()
{
return
this
.
getState
().
frames
[
0
];
return
this
.
getState
().
frames
[
0
];
},
},
focusedGroupId
()
{
setDefault
:
sinon
.
stub
(),
const
group
=
this
.
getState
().
groups
.
focusedGroup
;
return
group
?
group
.
id
:
null
;
},
setDirectLinkedGroupFetchFailed
:
sinon
.
stub
(),
setDirectLinkedGroupFetchFailed
:
sinon
.
stub
(),
clearDirectLinkedGroupFetchFailed
:
sinon
.
stub
(),
clearDirectLinkedGroupFetchFailed
:
sinon
.
stub
(),
}
}
);
);
fakeSession
=
sessionWithThreeGroups
();
fakeSession
=
sessionWithThreeGroups
();
fakeIsSidebar
=
true
;
fakeIsSidebar
=
true
;
fakeLocalStorage
=
{
getItem
:
sinon
.
stub
(),
setItem
:
sinon
.
stub
(),
};
fakeRootScope
=
{
fakeRootScope
=
{
eventCallbacks
:
{},
eventCallbacks
:
{},
...
@@ -129,6 +130,14 @@ describe('groups', function() {
...
@@ -129,6 +130,14 @@ describe('groups', function() {
};
};
fakeServiceUrl
=
sinon
.
stub
();
fakeServiceUrl
=
sinon
.
stub
();
fakeSettings
=
{
group
:
null
};
fakeSettings
=
{
group
:
null
};
$imports
.
$mock
({
'../util/annotation-metadata'
:
fakeMetadata
,
});
});
afterEach
(()
=>
{
$imports
.
$restore
();
});
});
function
service
()
{
function
service
()
{
...
@@ -137,7 +146,6 @@ describe('groups', function() {
...
@@ -137,7 +146,6 @@ describe('groups', function() {
fakeStore
,
fakeStore
,
fakeApi
,
fakeApi
,
fakeIsSidebar
,
fakeIsSidebar
,
fakeLocalStorage
,
fakeServiceUrl
,
fakeServiceUrl
,
fakeSession
,
fakeSession
,
fakeSettings
,
fakeSettings
,
...
@@ -145,6 +153,81 @@ describe('groups', function() {
...
@@ -145,6 +153,81 @@ describe('groups', function() {
);
);
}
}
describe
(
'#focus'
,
()
=>
{
it
(
'updates the focused group in the store'
,
()
=>
{
const
svc
=
service
();
fakeStore
.
focusedGroupId
.
returns
(
'whatever'
);
svc
.
focus
(
'whatnot'
);
assert
.
calledOnce
(
fakeStore
.
focusGroup
);
assert
.
calledWith
(
fakeStore
.
focusGroup
,
'whatnot'
);
});
context
(
'focusing to a different group than before'
,
()
=>
{
beforeEach
(()
=>
{
fakeStore
.
focusedGroupId
.
returns
(
'newgroup'
);
fakeStore
.
focusedGroupId
.
onFirstCall
().
returns
(
'whatnot'
);
});
it
(
'moves top-level annotations to the newly-focused group'
,
()
=>
{
const
fakeAnnotations
=
[
{
$tag
:
'1'
,
group
:
'groupA'
},
{
$tag
:
'2'
,
group
:
'groupB'
},
];
fakeMetadata
.
isReply
.
returns
(
false
);
fakeStore
.
newAnnotations
.
returns
(
fakeAnnotations
);
const
svc
=
service
();
svc
.
focus
(
'newgroup'
);
assert
.
calledWith
(
fakeStore
.
addAnnotations
,
sinon
.
match
([
{
$tag
:
'1'
,
group
:
'newgroup'
},
{
$tag
:
'2'
,
group
:
'newgroup'
},
])
);
const
updatedAnnotations
=
fakeStore
.
addAnnotations
.
getCall
(
0
).
args
[
0
];
updatedAnnotations
.
forEach
(
annot
=>
{
assert
.
equal
(
annot
.
group
,
'newgroup'
);
});
});
it
(
'does not move replies to the newly-focused group'
,
()
=>
{
fakeMetadata
.
isReply
.
returns
(
true
);
fakeStore
.
newAnnotations
.
returns
([
{
$tag
:
'1'
,
group
:
'groupA'
},
{
$tag
:
'2'
,
group
:
'groupB'
},
]);
const
svc
=
service
();
svc
.
focus
(
'newgroup'
);
assert
.
calledTwice
(
fakeMetadata
.
isReply
);
assert
.
notCalled
(
fakeStore
.
addAnnotations
);
});
it
(
'updates the focused-group default'
,
()
=>
{
const
svc
=
service
();
svc
.
focus
(
'newgroup'
);
assert
.
calledOnce
(
fakeStore
.
setDefault
);
assert
.
calledWith
(
fakeStore
.
setDefault
,
'focusedGroup'
,
'newgroup'
);
});
});
it
(
'does not update the focused-group default if the group has not changed'
,
()
=>
{
fakeStore
.
focusedGroupId
.
returns
(
'samegroup'
);
const
svc
=
service
();
svc
.
focus
(
'samegroup'
);
assert
.
notCalled
(
fakeStore
.
setDefault
);
});
});
describe
(
'#all'
,
function
()
{
describe
(
'#all'
,
function
()
{
it
(
'returns all groups from store.allGroups'
,
()
=>
{
it
(
'returns all groups from store.allGroups'
,
()
=>
{
const
svc
=
service
();
const
svc
=
service
();
...
@@ -157,7 +240,7 @@ describe('groups', function() {
...
@@ -157,7 +240,7 @@ describe('groups', function() {
describe
(
'#load'
,
function
()
{
describe
(
'#load'
,
function
()
{
it
(
'filters out direct-linked groups that are out of scope and scope enforced'
,
()
=>
{
it
(
'filters out direct-linked groups that are out of scope and scope enforced'
,
()
=>
{
const
svc
=
service
();
const
svc
=
service
();
fake
LocalStorage
.
getItem
.
returns
(
dummyGroups
[
0
].
id
);
fake
Store
.
getDefault
.
returns
(
dummyGroups
[
0
].
id
);
const
outOfScopeEnforcedGroup
=
{
const
outOfScopeEnforcedGroup
=
{
id
:
'oos'
,
id
:
'oos'
,
scopes
:
{
enforced
:
true
,
uri_patterns
:
[
'http://foo.com'
]
},
scopes
:
{
enforced
:
true
,
uri_patterns
:
[
'http://foo.com'
]
},
...
@@ -180,7 +263,7 @@ describe('groups', function() {
...
@@ -180,7 +263,7 @@ describe('groups', function() {
it
(
'catches error from api.group.read request'
,
()
=>
{
it
(
'catches error from api.group.read request'
,
()
=>
{
const
svc
=
service
();
const
svc
=
service
();
fake
LocalStorage
.
getItem
.
returns
(
dummyGroups
[
0
].
id
);
fake
Store
.
getDefault
.
returns
(
dummyGroups
[
0
].
id
);
fakeStore
.
setState
({
fakeStore
.
setState
({
directLinked
:
{
directLinked
:
{
directLinkedGroupId
:
'does-not-exist'
,
directLinkedGroupId
:
'does-not-exist'
,
...
@@ -325,7 +408,7 @@ describe('groups', function() {
...
@@ -325,7 +408,7 @@ describe('groups', function() {
it
(
'sets the focused group from the value saved in local storage'
,
()
=>
{
it
(
'sets the focused group from the value saved in local storage'
,
()
=>
{
const
svc
=
service
();
const
svc
=
service
();
fake
LocalStorage
.
getItem
.
returns
(
dummyGroups
[
1
].
id
);
fake
Store
.
getDefault
.
returns
(
dummyGroups
[
1
].
id
);
return
svc
.
load
().
then
(()
=>
{
return
svc
.
load
().
then
(()
=>
{
assert
.
calledWith
(
fakeStore
.
focusGroup
,
dummyGroups
[
1
].
id
);
assert
.
calledWith
(
fakeStore
.
focusGroup
,
dummyGroups
[
1
].
id
);
});
});
...
@@ -339,7 +422,7 @@ describe('groups', function() {
...
@@ -339,7 +422,7 @@ describe('groups', function() {
directLinkedGroupId
:
dummyGroups
[
1
].
id
,
directLinkedGroupId
:
dummyGroups
[
1
].
id
,
},
},
});
});
fake
LocalStorage
.
getItem
.
returns
(
dummyGroups
[
0
].
id
);
fake
Store
.
getDefault
.
returns
(
dummyGroups
[
0
].
id
);
fakeApi
.
groups
.
list
.
returns
(
Promise
.
resolve
(
dummyGroups
));
fakeApi
.
groups
.
list
.
returns
(
Promise
.
resolve
(
dummyGroups
));
fakeApi
.
annotation
.
get
.
returns
(
fakeApi
.
annotation
.
get
.
returns
(
Promise
.
resolve
({
Promise
.
resolve
({
...
@@ -360,7 +443,7 @@ describe('groups', function() {
...
@@ -360,7 +443,7 @@ describe('groups', function() {
},
},
});
});
fakeApi
.
groups
.
list
.
returns
(
Promise
.
resolve
(
dummyGroups
));
fakeApi
.
groups
.
list
.
returns
(
Promise
.
resolve
(
dummyGroups
));
fake
LocalStorage
.
getItem
.
returns
(
dummyGroups
[
0
].
id
);
fake
Store
.
getDefault
.
returns
(
dummyGroups
[
0
].
id
);
fakeApi
.
annotation
.
get
.
returns
(
fakeApi
.
annotation
.
get
.
returns
(
Promise
.
resolve
({
Promise
.
resolve
({
id
:
'ann-id'
,
id
:
'ann-id'
,
...
@@ -379,7 +462,7 @@ describe('groups', function() {
...
@@ -379,7 +462,7 @@ describe('groups', function() {
directLinkedGroupId
:
dummyGroups
[
1
].
id
,
directLinkedGroupId
:
dummyGroups
[
1
].
id
,
},
},
});
});
fake
LocalStorage
.
getItem
.
returns
(
dummyGroups
[
0
].
id
);
fake
Store
.
getDefault
.
returns
(
dummyGroups
[
0
].
id
);
fakeApi
.
groups
.
list
.
returns
(
Promise
.
resolve
(
dummyGroups
));
fakeApi
.
groups
.
list
.
returns
(
Promise
.
resolve
(
dummyGroups
));
return
svc
.
load
().
then
(()
=>
{
return
svc
.
load
().
then
(()
=>
{
assert
.
calledWith
(
fakeStore
.
focusGroup
,
dummyGroups
[
1
].
id
);
assert
.
calledWith
(
fakeStore
.
focusGroup
,
dummyGroups
[
1
].
id
);
...
@@ -416,7 +499,7 @@ describe('groups', function() {
...
@@ -416,7 +499,7 @@ describe('groups', function() {
[
null
,
'some-group-id'
].
forEach
(
groupId
=>
{
[
null
,
'some-group-id'
].
forEach
(
groupId
=>
{
it
(
'does not set the focused group if not present in the groups list'
,
()
=>
{
it
(
'does not set the focused group if not present in the groups list'
,
()
=>
{
const
svc
=
service
();
const
svc
=
service
();
fake
LocalStorage
.
getItem
.
returns
(
groupId
);
fake
Store
.
getDefault
.
returns
(
groupId
);
return
svc
.
load
().
then
(()
=>
{
return
svc
.
load
().
then
(()
=>
{
assert
.
notCalled
(
fakeStore
.
focusGroup
);
assert
.
notCalled
(
fakeStore
.
focusGroup
);
});
});
...
@@ -745,69 +828,6 @@ describe('groups', function() {
...
@@ -745,69 +828,6 @@ describe('groups', function() {
});
});
});
});
describe
(
'#focused'
,
function
()
{
it
(
'returns the focused group'
,
function
()
{
const
svc
=
service
();
fakeStore
.
setState
({
groups
:
{
groups
:
dummyGroups
,
focusedGroup
:
dummyGroups
[
2
]
},
});
assert
.
equal
(
svc
.
focused
(),
dummyGroups
[
2
]);
});
});
describe
(
'#focus'
,
function
()
{
it
(
'sets the focused group to the named group'
,
function
()
{
const
svc
=
service
();
svc
.
focus
(
'foo'
);
assert
.
calledWith
(
fakeStore
.
focusGroup
,
'foo'
);
});
});
context
(
'when the focused group changes'
,
()
=>
{
it
(
'stores the focused group id in localStorage'
,
function
()
{
service
();
fakeStore
.
setState
({
groups
:
{
groups
:
dummyGroups
,
focusedGroup
:
dummyGroups
[
1
]
},
});
assert
.
calledWithMatch
(
fakeLocalStorage
.
setItem
,
sinon
.
match
.
any
,
dummyGroups
[
1
].
id
);
});
it
(
'emits the GROUP_FOCUSED event if the focused group changed'
,
function
()
{
service
();
fakeStore
.
setState
({
groups
:
{
groups
:
dummyGroups
,
focusedGroup
:
dummyGroups
[
1
]
},
});
assert
.
calledWith
(
fakeRootScope
.
$broadcast
,
events
.
GROUP_FOCUSED
,
dummyGroups
[
1
].
id
);
});
it
(
'does not emit GROUP_FOCUSED if the focused group did not change'
,
()
=>
{
service
();
fakeStore
.
setState
({
groups
:
{
groups
:
dummyGroups
,
focusedGroup
:
dummyGroups
[
1
]
},
});
fakeRootScope
.
$broadcast
.
reset
();
fakeStore
.
setState
({
groups
:
{
groups
:
dummyGroups
,
focusedGroup
:
dummyGroups
[
1
]
},
});
assert
.
notCalled
(
fakeRootScope
.
$broadcast
);
});
});
describe
(
'#leave'
,
function
()
{
describe
(
'#leave'
,
function
()
{
it
(
'should call the group leave API'
,
function
()
{
it
(
'should call the group leave API'
,
function
()
{
const
s
=
service
();
const
s
=
service
();
...
...
src/sidebar/services/test/persisted-defaults-test.js
View file @
0a44e65b
...
@@ -3,6 +3,7 @@ import persistedDefaults from '../persisted-defaults';
...
@@ -3,6 +3,7 @@ import persistedDefaults from '../persisted-defaults';
const
DEFAULT_KEYS
=
{
const
DEFAULT_KEYS
=
{
annotationPrivacy
:
'hypothesis.privacy'
,
annotationPrivacy
:
'hypothesis.privacy'
,
focusedGroup
:
'hypothesis.groups.focus'
,
};
};
describe
(
'sidebar/services/persisted-defaults'
,
function
()
{
describe
(
'sidebar/services/persisted-defaults'
,
function
()
{
...
@@ -41,14 +42,15 @@ describe('sidebar/services/persisted-defaults', function() {
...
@@ -41,14 +42,15 @@ describe('sidebar/services/persisted-defaults', function() {
svc
.
init
();
svc
.
init
();
// Retrieving the one default from localStorage
// Retrieving each known default from localStorage...
assert
.
calledOnce
(
fakeLocalStorage
.
getItem
);
assert
.
equal
(
assert
.
calledWith
(
fakeLocalStorage
.
getItem
.
callCount
,
fakeLocalStorage
.
getItem
,
Object
.
keys
(
DEFAULT_KEYS
).
length
DEFAULT_KEYS
.
annotationPrivacy
);
);
// Setting the one default in the store
assert
.
calledOnce
(
fakeStore
.
setDefault
);
Object
.
keys
(
DEFAULT_KEYS
).
forEach
(
defaultKey
=>
{
assert
.
calledWith
(
fakeLocalStorage
.
getItem
,
DEFAULT_KEYS
[
defaultKey
]);
});
});
});
it
(
'should set defaults on the store with the values returned by `localStorage`'
,
()
=>
{
it
(
'should set defaults on the store with the values returned by `localStorage`'
,
()
=>
{
...
@@ -57,7 +59,9 @@ describe('sidebar/services/persisted-defaults', function() {
...
@@ -57,7 +59,9 @@ describe('sidebar/services/persisted-defaults', function() {
svc
.
init
();
svc
.
init
();
assert
.
calledWith
(
fakeStore
.
setDefault
,
'annotationPrivacy'
,
'bananas'
);
Object
.
keys
(
DEFAULT_KEYS
).
forEach
(
defaultKey
=>
{
assert
.
calledWith
(
fakeStore
.
setDefault
,
defaultKey
,
'bananas'
);
});
});
});
it
(
'should set default to `null` if key non-existent in storage'
,
()
=>
{
it
(
'should set default to `null` if key non-existent in storage'
,
()
=>
{
...
@@ -66,7 +70,9 @@ describe('sidebar/services/persisted-defaults', function() {
...
@@ -66,7 +70,9 @@ describe('sidebar/services/persisted-defaults', function() {
svc
.
init
();
svc
.
init
();
assert
.
calledWith
(
fakeStore
.
setDefault
,
'annotationPrivacy'
,
null
);
Object
.
keys
(
DEFAULT_KEYS
).
forEach
(
defaultKey
=>
{
assert
.
calledWith
(
fakeStore
.
setDefault
,
defaultKey
,
null
);
});
});
});
context
(
'when defaults change in the store'
,
()
=>
{
context
(
'when defaults change in the store'
,
()
=>
{
...
@@ -113,7 +119,7 @@ describe('sidebar/services/persisted-defaults', function() {
...
@@ -113,7 +119,7 @@ describe('sidebar/services/persisted-defaults', function() {
});
});
it
(
'should not update local storage if default has not changed'
,
()
=>
{
it
(
'should not update local storage if default has not changed'
,
()
=>
{
const
defaults
=
{
annotationPrivacy
:
'carrots'
};
const
defaults
=
{
focusedGroup
:
'carrots'
};
fakeLocalStorage
.
getItem
.
returns
(
'carrots'
);
fakeLocalStorage
.
getItem
.
returns
(
'carrots'
);
fakeStore
.
getDefaults
.
returns
(
defaults
);
fakeStore
.
getDefaults
.
returns
(
defaults
);
fakeStore
.
setState
({
defaults
:
defaults
});
fakeStore
.
setState
({
defaults
:
defaults
});
...
...
src/sidebar/services/test/root-thread-test.js
View file @
0a44e65b
...
@@ -416,40 +416,4 @@ describe('rootThread', function() {
...
@@ -416,40 +416,4 @@ describe('rootThread', function() {
});
});
});
});
});
});
describe
(
'when the focused group changes'
,
function
()
{
it
(
'moves new annotations to the focused group'
,
function
()
{
fakeStore
.
state
.
annotations
.
annotations
=
[{
$tag
:
'a-tag'
}];
$rootScope
.
$broadcast
(
events
.
GROUP_FOCUSED
,
'private-group'
);
assert
.
calledWith
(
fakeStore
.
addAnnotations
,
sinon
.
match
([
{
$tag
:
'a-tag'
,
group
:
'private-group'
,
},
])
);
});
it
(
'does not move replies to the new group'
,
function
()
{
fakeStore
.
state
.
annotations
.
annotations
=
[
annotationFixtures
.
newReply
()];
$rootScope
.
$broadcast
(
events
.
GROUP_FOCUSED
,
'private-group'
);
assert
.
notCalled
(
fakeStore
.
addAnnotations
);
});
it
(
'does not move saved annotations to the new group'
,
function
()
{
fakeStore
.
state
.
annotations
.
annotations
=
[
annotationFixtures
.
defaultAnnotation
(),
];
$rootScope
.
$broadcast
(
events
.
GROUP_FOCUSED
,
'private-group'
);
assert
.
notCalled
(
fakeStore
.
addAnnotations
);
});
});
});
});
src/sidebar/store/modules/annotations.js
View file @
0a44e65b
...
@@ -383,6 +383,16 @@ function findAnnotationByID(state, id) {
...
@@ -383,6 +383,16 @@ function findAnnotationByID(state, id) {
return
findByID
(
state
.
annotations
.
annotations
,
id
);
return
findByID
(
state
.
annotations
.
annotations
,
id
);
}
}
/**
* Return all loaded annotations that are not highlights and have not been saved
* to the server.
*/
const
newAnnotations
=
createSelector
(
state
=>
state
.
annotations
.
annotations
,
annotations
=>
annotations
.
filter
(
ann
=>
metadata
.
isNew
(
ann
)
&&
!
metadata
.
isHighlight
(
ann
))
);
/**
/**
* Return all loaded annotations that are highlights and have not been saved
* Return all loaded annotations that are highlights and have not been saved
* to the server.
* to the server.
...
@@ -445,6 +455,7 @@ export default {
...
@@ -445,6 +455,7 @@ export default {
findAnnotationByID
,
findAnnotationByID
,
findIDsForTags
,
findIDsForTags
,
isWaitingToAnchorAnnotations
,
isWaitingToAnchorAnnotations
,
newAnnotations
,
newHighlights
,
newHighlights
,
noteCount
,
noteCount
,
orphanCount
,
orphanCount
,
...
...
src/sidebar/store/modules/defaults.js
View file @
0a44e65b
...
@@ -23,6 +23,7 @@ function init() {
...
@@ -23,6 +23,7 @@ function init() {
*/
*/
return
{
return
{
annotationPrivacy
:
null
,
annotationPrivacy
:
null
,
focusedGroup
:
null
,
};
};
}
}
...
...
src/sidebar/store/modules/test/annotations-test.js
View file @
0a44e65b
...
@@ -174,6 +174,41 @@ describe('sidebar/store/modules/annotations', function() {
...
@@ -174,6 +174,41 @@ describe('sidebar/store/modules/annotations', function() {
});
});
});
});
describe
(
'newAnnotations'
,
()
=>
{
[
{
annotations
:
[
fixtures
.
oldAnnotation
(),
// no
fixtures
.
newAnnotation
(),
// yes
fixtures
.
newAnnotation
(),
// yes
fixtures
.
newReply
(),
// yes
],
expectedLength
:
3
,
},
{
annotations
:
[
fixtures
.
oldAnnotation
(),
fixtures
.
newHighlight
()],
expectedLength
:
0
,
},
{
annotations
:
[
fixtures
.
newHighlight
(),
// no
fixtures
.
newReply
(),
// yes
fixtures
.
oldAnnotation
(),
// no
fixtures
.
newPageNote
(),
// yes
],
expectedLength
:
2
,
},
].
forEach
(
testCase
=>
{
it
(
'returns number of unsaved, new annotations'
,
()
=>
{
const
state
=
{
annotations
:
{
annotations
:
testCase
.
annotations
}
};
assert
.
lengthOf
(
selectors
.
newAnnotations
(
state
),
testCase
.
expectedLength
);
});
});
});
describe
(
'newHighlights'
,
()
=>
{
describe
(
'newHighlights'
,
()
=>
{
[
[
{
{
...
...
src/sidebar/store/modules/test/defaults-test.js
View file @
0a44e65b
...
@@ -50,7 +50,11 @@ describe('store/modules/defaults', function() {
...
@@ -50,7 +50,11 @@ describe('store/modules/defaults', function() {
const
latestDefaults
=
store
.
getDefaults
();
const
latestDefaults
=
store
.
getDefaults
();
assert
.
hasAllKeys
(
latestDefaults
,
[
'foo'
,
'annotationPrivacy'
]);
assert
.
hasAllKeys
(
latestDefaults
,
[
'foo'
,
'annotationPrivacy'
,
'focusedGroup'
,
]);
});
});
});
});
});
});
...
...
src/sidebar/test/annotation-fixtures.js
View file @
0a44e65b
...
@@ -104,6 +104,18 @@ export function newHighlight() {
...
@@ -104,6 +104,18 @@ export function newHighlight() {
};
};
}
}
/** Return an annotation domain model object for a new page note.
*/
export
function
newPageNote
()
{
return
{
$highlight
:
undefined
,
target
:
[{
source
:
'http://example.org'
}],
references
:
[],
text
:
''
,
tags
:
[],
};
}
/** Return an annotation domain model object for an existing annotation
/** Return an annotation domain model object for an existing annotation
* received from the server.
* received from the server.
*/
*/
...
@@ -137,7 +149,8 @@ export function oldHighlight() {
...
@@ -137,7 +149,8 @@ export function oldHighlight() {
*/
*/
export
function
oldPageNote
()
{
export
function
oldPageNote
()
{
return
{
return
{
highlight
:
undefined
,
id
:
'note_id'
,
$highlight
:
undefined
,
target
:
[{
source
:
'http://example.org'
}],
target
:
[{
source
:
'http://example.org'
}],
references
:
[],
references
:
[],
text
:
''
,
text
:
''
,
...
...
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