Commit ccf72650 authored by Sean Hammond's avatar Sean Hammond

Translate annotation-test.coffee to JavaScript

annotation-test.js is just the output from `coffee -c ...` untouched.
parent e09b9410
{module, inject} = angular.mock
events = require('../../events')
describe 'annotation', ->
$compile = null
$document = null
$element = null
$q = null
$rootScope = null
$scope = null
$timeout = null
$window = null
annotation = null
controller = null
isolateScope = null
fakeAnnotationMapper = null
fakeAnnotationUI = null
fakeDrafts = null
fakeFeatures = null
fakeFlash = null
fakeGroups = null
fakeMomentFilter = null
fakePermissions = null
fakePersonaFilter = null
fakeDocumentTitleFilter = null
fakeDocumentDomainFilter = null
fakeSession = null
fakeStore = null
fakeTags = null
fakeTime = null
fakeUrlEncodeFilter = null
sandbox = null
createDirective = ->
$element = angular.element('<div annotation="annotation">')
$compile($element)($scope)
$scope.$digest()
controller = $element.controller('annotation')
isolateScope = $element.isolateScope()
before ->
angular.module('h', [])
.directive('annotation', require('../annotation').directive)
beforeEach module('h')
beforeEach module('h.templates')
beforeEach module ($provide) ->
sandbox = sinon.sandbox.create()
fakeAnnotationMapper =
createAnnotation: sandbox.stub().returns
permissions:
read: ['acct:bill@localhost']
update: ['acct:bill@localhost']
destroy: ['acct:bill@localhost']
admin: ['acct:bill@localhost']
deleteAnnotation: sandbox.stub()
fakeAnnotationUI = {}
fakeDrafts = {
update: sandbox.stub()
remove: sandbox.stub()
get: sandbox.stub()
}
fakeFeatures = {
flagEnabled: sandbox.stub().returns(true)
}
fakeFlash = sandbox.stub()
fakeMomentFilter = sandbox.stub().returns('ages ago')
fakePermissions = {
isShared: sandbox.stub().returns(true)
isPrivate: sandbox.stub().returns(false)
permits: sandbox.stub().returns(true)
shared: sandbox.stub().returns({read: ['everybody']})
private: sandbox.stub().returns({read: ['justme']})
default: sandbox.stub().returns({read: ['default']})
setDefault: sandbox.stub()
}
fakePersonaFilter = sandbox.stub().returnsArg(0)
fakeDocumentTitleFilter = (arg) -> ''
fakeDocumentDomainFilter = (arg) -> ''
fakeSession =
state:
userid: 'acct:bill@localhost'
fakeTags = {
filter: sandbox.stub().returns('a while ago'),
store: sandbox.stub()
}
fakeTime = {
toFuzzyString: sandbox.stub().returns('a while ago')
nextFuzzyUpdate: sandbox.stub().returns(30)
}
fakeUrlEncodeFilter = (v) -> encodeURIComponent(v)
fakeGroups = {
focused: -> {}
get: ->
}
$provide.value 'annotationMapper', fakeAnnotationMapper
$provide.value 'annotationUI', fakeAnnotationUI
$provide.value 'drafts', fakeDrafts
$provide.value 'features', fakeFeatures
$provide.value 'flash', fakeFlash
$provide.value 'momentFilter', fakeMomentFilter
$provide.value 'permissions', fakePermissions
$provide.value 'personaFilter', fakePersonaFilter
$provide.value('documentTitleFilter', fakeDocumentTitleFilter)
$provide.value('documentDomainFilter', fakeDocumentDomainFilter)
$provide.value 'session', fakeSession
$provide.value 'store', fakeStore
$provide.value 'tags', fakeTags
$provide.value 'time', fakeTime
$provide.value 'urlencodeFilter', fakeUrlEncodeFilter
$provide.value 'groups', fakeGroups
return
beforeEach inject (_$compile_, _$document_, _$q_, _$rootScope_, _$timeout_,
_$window_) ->
$compile = _$compile_
$document = _$document_
$window = _$window_
$q = _$q_
$timeout = _$timeout_
$rootScope = _$rootScope_
$scope = $rootScope.$new()
$scope.annotation = annotation =
id: 'deadbeef'
document:
title: 'A special document'
target: [{}]
uri: 'http://example.com'
user: 'acct:bill@localhost'
afterEach ->
sandbox.restore()
describe 'when the annotation is a highlight', ->
beforeEach ->
annotation.$highlight = true
annotation.$create = sinon.stub().returns
then: angular.noop
catch: angular.noop
finally: angular.noop
it 'persists upon login', ->
delete annotation.id
delete annotation.user
fakeSession.state.userid = null
createDirective()
$scope.$digest()
assert.notCalled annotation.$create
fakeSession.state.userid = 'acct:ted@wyldstallyns.com'
$scope.$broadcast(events.USER_CHANGED, {})
$scope.$digest()
assert.calledOnce annotation.$create
it 'is private', ->
delete annotation.id
createDirective()
$scope.$digest()
assert.deepEqual annotation.permissions, {read: ['justme']}
describe '#reply', ->
container = null
beforeEach ->
createDirective()
annotation.permissions =
read: ['acct:joe@localhost']
update: ['acct:joe@localhost']
destroy: ['acct:joe@localhost']
admin: ['acct:joe@localhost']
it 'creates a new reply with the proper uri and references', ->
controller.reply()
match = sinon.match {references: [annotation.id], uri: annotation.uri}
assert.calledWith(fakeAnnotationMapper.createAnnotation, match)
it 'makes the annotation shared if the parent is shared', ->
reply = {}
fakeAnnotationMapper.createAnnotation.returns(reply)
fakePermissions.isShared.returns(true)
controller.reply()
assert.deepEqual(reply.permissions, {read: ['everybody']})
it 'makes the annotation shared if the parent is shared', ->
$scope.annotation.group = "my group"
$scope.annotation.permissions = {read: ["my group"]}
reply = {}
fakeAnnotationMapper.createAnnotation.returns(reply)
fakePermissions.isShared = (permissions, group) ->
return group in permissions.read
fakePermissions.shared = (group) ->
return {read: [group]}
controller.reply()
assert "my group" in reply.permissions.read
it 'does not add the world readable principal if the parent is private', ->
reply = {}
fakeAnnotationMapper.createAnnotation.returns(reply)
fakePermissions.isShared.returns(false)
controller.reply()
assert.deepEqual(reply.permissions, {read: ['justme']})
it "sets the reply's group to be the same as its parent's", ->
$scope.annotation.group = "my group"
reply = {}
fakeAnnotationMapper.createAnnotation.returns(reply)
controller.reply()
assert.equal(reply.group, $scope.annotation.group)
describe '#setPrivacy', ->
beforeEach ->
createDirective()
it 'makes the annotation private when level is "private"', ->
annotation.$update = sinon.stub().returns(Promise.resolve())
controller.edit();
controller.setPrivacy('private')
controller.save().then(->
# verify that the permissions are updated once the annotation
# is saved
assert.deepEqual(annotation.permissions, {read: ['justme']})
)
it 'makes the annotation shared when level is "shared"', ->
annotation.$update = sinon.stub().returns(Promise.resolve())
controller.edit();
controller.setPrivacy('shared')
controller.save().then(->
assert.deepEqual(annotation.permissions, {read: ['everybody']})
)
it 'saves the "shared" visibility level to localStorage', ->
annotation.$update = sinon.stub().returns(Promise.resolve())
controller.edit();
controller.setPrivacy('shared')
controller.save().then(->
assert(fakePermissions.setDefault.calledWithExactly('shared'))
)
it 'saves the "private" visibility level to localStorage', ->
annotation.$update = sinon.stub().returns(Promise.resolve())
controller.edit();
controller.setPrivacy('private')
controller.save().then(->
assert(fakePermissions.setDefault.calledWithExactly('private'))
)
it "doesn't save the visibility if the annotation is a reply", ->
annotation.$update = sinon.stub().returns(Promise.resolve())
annotation.references = ["parent id"]
controller.edit();
controller.setPrivacy('private')
controller.save().then(->
assert(not fakePermissions.setDefault.called)
)
describe '#hasContent', ->
beforeEach ->
createDirective()
it 'returns false if the annotation has no tags or text', ->
controller.annotation.text = ''
controller.annotation.tags = [];
assert.ok(!controller.hasContent())
it 'returns true if the annotation has tags or text', ->
controller.annotation.text = 'bar'
assert.ok(controller.hasContent())
controller.annotation.text = ''
controller.annotation.tags = [{text: 'foo'}]
assert.ok(controller.hasContent())
describe '#hasQuotes', ->
beforeEach ->
createDirective()
it 'returns false if the annotation has no quotes', ->
controller.annotation.target = [{}]
assert.isFalse(controller.hasQuotes())
it 'returns true if the annotation has quotes', ->
controller.annotation.target = [{
selector: [{
type: 'TextQuoteSelector'
}]
}]
assert.isTrue(controller.hasQuotes())
describe '#render', ->
beforeEach ->
createDirective()
sandbox.spy(controller, 'render')
afterEach ->
sandbox.restore()
it 'is called exactly once on model changes', ->
assert.notCalled(controller.render)
annotation.delete = true
$scope.$digest()
assert.calledOnce(controller.render)
annotation.booz = 'baz'
$scope.$digest()
assert.calledTwice(controller.render)
it 'provides a document title', ->
controller.render()
assert.equal(controller.document.title, 'A special document')
it 'uses the first title when there are more than one', ->
annotation.document.title = ['first title', 'second title']
controller.render()
assert.equal(controller.document.title, 'first title')
it 'truncates long titles', ->
annotation.document.title = '''A very very very long title that really
shouldn't be found on a page on the internet.'''
controller.render()
assert.equal(controller.document.title, 'A very very very long title th…')
it 'provides a document uri', ->
controller.render()
assert.equal(controller.document.uri, 'http://example.com')
it 'provides an extracted domain from the uri', ->
controller.render()
assert.equal(controller.document.domain, 'example.com')
it 'uses the domain for the title if the title is not present', ->
delete annotation.document.title
controller.render()
assert.equal(controller.document.title, 'example.com')
it 'still sets the uri correctly if the annotation has no document', ->
delete annotation.document
controller.render()
assert(controller.document.uri == $scope.annotation.uri)
it 'still sets the domain correctly if the annotation has no document', ->
delete annotation.document
controller.render()
assert(controller.document.domain == 'example.com')
it 'uses the domain for the title when the annotation has no document', ->
delete annotation.document
controller.render()
assert(controller.document.title == 'example.com')
describe 'timestamp', ->
clock = null
beforeEach ->
clock = sinon.useFakeTimers()
annotation.created = (new Date()).toString()
annotation.updated = (new Date()).toString()
afterEach ->
clock.restore()
it 'is not updated for unsaved annotations', ->
# Unsaved annotations don't have an updated time yet so a timestamp
# string can't be computed for them.
annotation.updated = null
$scope.$digest()
assert.equal(controller.timestamp, null)
it 'is updated on first digest', ->
$scope.$digest()
assert.equal(controller.timestamp, 'a while ago')
it 'is updated after a timeout', ->
fakeTime.nextFuzzyUpdate.returns(10)
fakeTime.toFuzzyString.returns('ages ago')
$scope.$digest()
clock.tick(11000)
$timeout.flush()
assert.equal(controller.timestamp, 'ages ago')
it 'is no longer updated after the scope is destroyed', ->
$scope.$digest()
$scope.$destroy()
$timeout.flush()
$timeout.verifyNoPendingTasks()
describe 'share', ->
dialog = null
beforeEach ->
dialog = $element.find('.share-dialog-wrapper')
it 'sets and unsets the open class on the share wrapper', ->
dialog.find('button').click()
isolateScope.$digest()
assert.ok(dialog.hasClass('open'))
$document.click()
assert.notOk(dialog.hasClass('open'))
describe "deleteAnnotation() method", ->
beforeEach ->
createDirective()
fakeAnnotationMapper.deleteAnnotation = sandbox.stub()
fakeFlash.error = sandbox.stub()
it "calls annotationMapper.delete() if the delete is confirmed", (done) ->
sandbox.stub($window, 'confirm').returns(true)
fakeAnnotationMapper.deleteAnnotation.returns($q.resolve())
controller.delete().then ->
assert fakeAnnotationMapper.deleteAnnotation.calledWith(annotation)
done()
$timeout.flush()
it "doesn't call annotationMapper.delete() if the delete is cancelled", ->
sandbox.stub($window, 'confirm').returns(false)
assert fakeAnnotationMapper.deleteAnnotation.notCalled
it "flashes a generic error if the server cannot be reached", (done) ->
sandbox.stub($window, 'confirm').returns(true)
fakeAnnotationMapper.deleteAnnotation.returns($q.reject({status: 0}))
controller.delete().then ->
assert fakeFlash.error.calledWith(
"Service unreachable.", "Deleting annotation failed")
done()
$timeout.flush()
it "flashes an error if the delete fails on the server", (done) ->
sandbox.stub($window, 'confirm').returns(true)
fakeAnnotationMapper.deleteAnnotation.returns($q.reject({
status: 500,
statusText: "Server Error",
data: {}
})
)
controller.delete().then ->
assert fakeFlash.error.calledWith(
"500 Server Error", "Deleting annotation failed")
done()
$timeout.flush()
it "doesn't flash an error if the delete succeeds", (done) ->
sandbox.stub($window, 'confirm').returns(true)
fakeAnnotationMapper.deleteAnnotation.returns($q.resolve())
controller.delete().then ->
assert fakeFlash.error.notCalled
done()
$timeout.flush()
describe "saving a new annotation", ->
beforeEach ->
createDirective()
fakeFlash.error = sandbox.stub()
controller.action = 'create'
annotation.$create = sandbox.stub()
it "emits annotationCreated when saving an annotation succeeds", (done) ->
sandbox.spy($rootScope, '$emit')
annotation.$create.returns(Promise.resolve())
controller.save().then ->
assert $rootScope.$emit.calledWith("annotationCreated")
done()
it "flashes a generic error if the server cannot be reached", (done) ->
annotation.$create.returns(Promise.reject({status: 0}))
controller.save().then ->
assert fakeFlash.error.calledWith(
"Service unreachable.", "Saving annotation failed")
done()
$timeout.flush()
it "flashes an error if saving the annotation fails on the server", (done) ->
annotation.$create.returns(Promise.reject({
status: 500,
statusText: "Server Error",
data: {}
})
)
controller.save().then ->
assert fakeFlash.error.calledWith(
"500 Server Error", "Saving annotation failed")
done()
it "doesn't flash an error when saving an annotation succeeds", ->
annotation.$create.returns(Promise.resolve())
controller.save()
assert fakeFlash.error.notCalled
describe "saving an edited an annotation", ->
beforeEach ->
createDirective()
fakeFlash.error = sandbox.stub()
controller.action = 'edit'
annotation.$update = sandbox.stub()
it "flashes a generic error if the server cannot be reached", (done) ->
annotation.$update.returns(Promise.reject({status: 0}))
controller.save().then ->
assert fakeFlash.error.calledWith(
"Service unreachable.", "Saving annotation failed")
done()
it "flashes an error if saving the annotation fails on the server", (done) ->
annotation.$update.returns(Promise.reject({
status: 500,
statusText: "Server Error",
data: {}
})
)
controller.save().then ->
assert fakeFlash.error.calledWith(
"500 Server Error", "Saving annotation failed")
done()
it "doesn't flash an error if saving the annotation succeeds", ->
annotation.$update.returns(Promise.resolve())
controller.save()
assert fakeFlash.error.notCalled
describe "drafts", ->
it "creates a draft when editing an annotation", ->
createDirective()
controller.edit()
assert.calledWith(fakeDrafts.update, annotation)
it "creates a draft with only editable fields which are non-null", ->
# When a draft is saved, we shouldn't save any fields to the draft
# "changes" object that aren't actually set on the annotation. In this
# case, both permissions and tags are null so shouldn't be saved in the
# draft.
createDirective()
annotation.permissions = null
annotation.text = 'Hello!'
annotation.tags = null
controller.edit()
assert.calledWith(fakeDrafts.update, annotation, {text: 'Hello!'})
it "starts editing immediately if there is a draft", ->
fakeDrafts.get.returns({
tags: [{text: 'unsaved'}]
text: 'unsaved-text'
})
createDirective()
assert.isTrue(controller.editing)
it "uses the text and tags from the draft if present", ->
fakeDrafts.get.returns({
tags: ['unsaved-tag']
text: 'unsaved-text'
})
createDirective()
assert.deepEqual(controller.annotation.tags, [{text: 'unsaved-tag'}])
assert.equal(controller.annotation.text, 'unsaved-text')
it "removes the draft when changes are discarded", ->
createDirective()
controller.edit()
controller.revert()
assert.calledWith(fakeDrafts.remove, annotation)
it "removes the draft when changes are saved", ->
annotation.$update = sandbox.stub().returns(Promise.resolve())
createDirective()
controller.edit()
controller.save()
# the controller currently removes the draft whenever an annotation
# update is committed on the server. This can happen either when saving
# locally or when an update is committed in another instance of H
# which is then pushed to the current instance
annotation.updated = (new Date()).toISOString()
$scope.$digest()
assert.calledWith(fakeDrafts.remove, annotation)
describe "when the focused group changes", ->
it "updates the current draft", ->
createDirective()
controller.edit()
controller.annotation.text = 'unsaved-text'
controller.annotation.tags = []
controller.annotation.permissions = 'new permissions'
fakeDrafts.get = sinon.stub().returns({text: 'old-draft'})
fakeDrafts.update = sinon.stub()
$rootScope.$broadcast(events.GROUP_FOCUSED)
assert.calledWith(fakeDrafts.update, annotation, {
text: 'unsaved-text',
tags: []
permissions: 'new permissions'
})
it "should not create a new draft", ->
createDirective()
controller.edit()
fakeDrafts.update = sinon.stub()
fakeDrafts.get = sinon.stub().returns(null)
$rootScope.$broadcast(events.GROUP_FOCUSED)
assert.notCalled(fakeDrafts.update)
it "moves new annotations to the focused group", ->
annotation.id = null
createDirective()
fakeGroups.focused = sinon.stub().returns({id: 'new-group'})
$rootScope.$broadcast(events.GROUP_FOCUSED)
assert.equal(annotation.group, 'new-group')
it "updates perms when moving new annotations to the focused group", ->
# id must be null so that AnnotationController considers this a new
# annotation.
annotation.id = null
annotation.group = 'old-group'
annotation.permissions = {read: [annotation.group]}
# This is a shared annotation.
fakePermissions.isShared.returns(true)
createDirective()
# Make permissions.shared() behave like we expect it to.
fakePermissions.shared = (groupId) ->
return {read: [groupId]}
fakeGroups.focused = sinon.stub().returns({id: 'new-group'})
$rootScope.$broadcast(events.GROUP_FOCUSED)
assert.deepEqual(annotation.permissions.read, ['new-group'])
it "saves shared permissions for the new group to drafts", ->
# id must be null so that AnnotationController considers this a new
# annotation.
annotation.id = null
annotation.group = 'old-group'
annotation.permissions = {read: [annotation.group]}
# This is a shared annotation.
fakePermissions.isShared.returns(true)
createDirective()
# drafts.get() needs to return something truthy, otherwise
# AnnotationController won't try to update the draft for the annotation.
fakeDrafts.get.returns(true)
# Make permissions.shared() behave like we expect it to.
fakePermissions.shared = (groupId) ->
return {read: [groupId]}
# Change the focused group.
fakeGroups.focused = sinon.stub().returns({id: 'new-group'})
$rootScope.$broadcast(events.GROUP_FOCUSED)
assert.deepEqual(
fakeDrafts.update.lastCall.args[1].permissions.read,
['new-group'],
"Shared permissions for the new group should be saved to drafts")
it "does not change perms when moving new private annotations", ->
# id must be null so that AnnotationController considers this a new
# annotation.
annotation.id = null
annotation.group = 'old-group'
annotation.permissions = {read: ['acct:bill@localhost']}
createDirective()
# This is a private annotation.
fakePermissions.isShared.returns(false)
fakeGroups.focused = sinon.stub().returns({id: 'new-group'})
$rootScope.$broadcast(events.GROUP_FOCUSED)
assert.deepEqual(annotation.permissions.read, ['acct:bill@localhost'],
'The annotation should still be private')
describe("AnnotationController", ->
before(->
angular.module("h", [])
.directive("annotation", require("../annotation").directive)
)
beforeEach(module("h"))
beforeEach(module("h.templates"))
# Return Angular's $compile service.
getCompileService = ->
$compile = null
inject((_$compile_) ->
$compile = _$compile_
)
$compile
# Return Angular's $rootScope.
getRootScope = ->
$rootScope = null
inject((_$rootScope_) ->
$rootScope = _$rootScope_
)
$rootScope
###
Return an annotation directive instance and stub services etc.
###
createAnnotationDirective = ({annotation, personaFilter, momentFilter,
urlencodeFilter, drafts, features, flash,
permissions, session, tags, time, annotationUI,
annotationMapper, groups, documentTitleFilter,
documentDomainFilter, localStorage}) ->
session = session or {state: {userid: "acct:fred@hypothes.is"}}
locals = {
personaFilter: personaFilter or ->
momentFilter: momentFilter or {}
urlencodeFilter: urlencodeFilter or {}
drafts: drafts or {
update: ->
remove: ->
get: ->
}
features: features or {
flagEnabled: -> true
}
flash: flash or {
info: ->
error: ->
}
permissions: permissions or {
isShared: (permissions, group) ->
return group in permissions.read
isPrivate: (permissions, user) ->
return user in permissions.read
permits: -> true
shared: -> {}
private: -> {}
default: -> {}
setDefault: ->
}
session: session
tags: tags or {store: ->}
time: time or {
toFuzzyString: ->
nextFuzzyUpdate: ->
}
annotationUI: annotationUI or {}
annotationMapper: annotationMapper or {}
groups: groups or {
get: ->
focused: -> {}
}
documentTitleFilter: documentTitleFilter or -> ''
documentDomainFilter: documentDomainFilter or -> ''
localStorage: localStorage or {
setItem: ->
getItem: ->
}
}
module(($provide) ->
$provide.value("personaFilter", locals.personaFilter)
$provide.value("momentFilter", locals.momentFilter)
$provide.value("urlencodeFilter", locals.urlencodeFilter)
$provide.value("drafts", locals.drafts)
$provide.value("features", locals.features)
$provide.value("flash", locals.flash)
$provide.value("permissions", locals.permissions)
$provide.value("session", locals.session)
$provide.value("tags", locals.tags)
$provide.value("time", locals.time)
$provide.value("annotationUI", locals.annotationUI)
$provide.value("annotationMapper", locals.annotationMapper)
$provide.value("groups", locals.groups)
$provide.value("documentTitleFilter", locals.documentTitleFilter)
$provide.value("documentDomainFilter", locals.documentDomainFilter)
$provide.value("localStorage", locals.localStorage)
return
)
locals.element = angular.element('<div annotation="annotation">')
compiledElement = getCompileService()(locals.element)
locals.$rootScope = getRootScope()
locals.parentScope = locals.$rootScope.$new()
locals.parentScope.annotation = annotation or {}
locals.directive = compiledElement(locals.parentScope)
locals.$rootScope.$digest()
locals.controller = locals.element.controller("annotation")
locals.isolateScope = locals.element.isolateScope()
locals
describe("createAnnotationDirective", ->
it("creates the directive without crashing", ->
createAnnotationDirective({})
)
)
it("sets the user of new annotations", ->
annotation = {}
{session} = createAnnotationDirective({annotation: annotation})
assert.equal(annotation.user, session.state.userid)
)
it("sets the permissions of new annotations", ->
# This is a new annotation, doesn't have any permissions yet.
annotation = {group: "test-group"}
permissions = {
default: sinon.stub().returns("default permissions")
isShared: ->
isPrivate: ->
}
createAnnotationDirective({
annotation: annotation, permissions: permissions})
assert(permissions.default.calledWithExactly("test-group"))
assert.equal(annotation.permissions, "default permissions",
"It should set a new annotation's permissions to what
permissions.default() returns")
)
it("doesn't overwrite permissions if the annotation already has them", ->
# The annotation will already have some permissions, before it's
# passed to AnnotationController.
annotation = {
permissions: {
read: ["foo"]
update: ["bar"]
delete: ["gar"]
admin: ["har"]
}
}
# Save the original permissions for asserting against later.
original_permissions = JSON.parse(JSON.stringify(annotation.permissions))
permissions = {
default: sinon.stub().returns("new permissions")
isShared: ->
isPrivate: ->
}
createAnnotationDirective(
{annotation: annotation, permissions: permissions})
assert.deepEqual(annotation.permissions, original_permissions)
)
describe("save", ->
it("Passes group:<id> to the server when saving a new annotation", ->
annotation = {
# The annotation needs to have a user or the controller will refuse to
# save it.
user: 'acct:fred@hypothes.is'
# The annotation needs to have some text or it won't validate.
text: 'foo'
}
# Stub $create so we can spy on what gets sent to the server.
annotation.$create = sinon.stub().returns(Promise.resolve())
group = {id: "test-id"}
{controller} = createAnnotationDirective({
annotation: annotation
# Mock the groups service, pretend that there's a group with id
# "test-group" focused.
groups: {
focused: -> group
get: ->
}
})
controller.action = 'create'
controller.save().then(->
assert annotation.$create.lastCall.thisValue.group == "test-id"
)
)
)
describe("when the user signs in", ->
it("sets the user of unsaved annotations", ->
# This annotation has no user yet, because that's what happens
# when you create a new annotation while not signed in.
annotation = {}
session = {state: {userid: null}} # Not signed in.
{$rootScope} = createAnnotationDirective(
{annotation: annotation, session:session})
# At this point we would not expect the user to have been set,
# even though the annotation has been created, because the user isn't
# signed in.
assert(!annotation.user)
# Sign the user in.
session.state.userid = "acct:fred@hypothes.is"
# The session service would broadcast USER_CHANGED after sign in.
$rootScope.$broadcast(events.USER_CHANGED, {})
assert.equal(annotation.user, session.state.userid)
)
it("sets the permissions of unsaved annotations", ->
# This annotation has no permissions yet, because that's what happens
# when you create a new annotation while not signed in.
annotation = {group: "__world__"}
session = {state: {userid: null}} # Not signed in.
permissions = {
# permissions.default() would return null, because the user isn't
# signed in.
default: -> null
# We just need to mock these two functions so that it doesn't crash.
isShared: ->
isPrivate: ->
}
{$rootScope} = createAnnotationDirective(
{annotation: annotation, session:session, permissions: permissions})
# At this point we would not expect the permissions to have been set,
# even though the annotation has been created, because the user isn't
# signed in.
assert(!annotation.permissions)
# Sign the user in.
session.state.userid = "acct:fred@hypothes.is"
# permissions.default() would now return permissions, because the user
# is signed in.
permissions.default = -> "__default_permissions__"
# The session service would broadcast USER_CHANGED after sign in.
$rootScope.$broadcast(events.USER_CHANGED, {})
assert.equal(annotation.permissions, "__default_permissions__")
)
)
###
Simulate what happens when the user edits an annotation, clicks Save,
gets an error because the server fails to save the annotation, then clicks
Cancel - in the frontend the annotation should be restored to its original
value, the edits lost.
###
it "restores the original text when editing is cancelled", ->
{controller} = createAnnotationDirective(
annotation: {
id: "test-annotation-id"
user: "acct:bill@localhost"
text: "Initial annotation body text"
# Allow the initial save of the annotation to succeed.
$create: ->
Promise.resolve()
# Simulate saving the edit of the annotation to the server failing.
$update: ->
Promise.reject({
status: 500,
statusText: "Server Error",
data: {}
})
}
)
original_text = controller.annotation.text
# Simulate the user clicking the Edit button on the annotation.
controller.edit()
# Simulate the user typing some text into the annotation editor textarea.
controller.annotation.text = "changed by test code"
# Simulate the user hitting the Save button and wait for the
# (unsuccessful) response from the server.
controller.save()
# At this point the annotation editor controls are still open, and the
# annotation's text is still the modified (unsaved) text.
assert controller.annotation.text == "changed by test code"
# Simulate the user clicking the Cancel button.
controller.revert()
# Now the original text should be restored.
assert controller.annotation.text == original_text
# test that editing reverting changes to an annotation with
# no text resets the text to be empty
it "clears the text when reverting changes to a highlight", ->
{controller} = createAnnotationDirective({
annotation: {
id: "test-annotation-id",
user: "acct:bill@localhost"
}
})
controller.edit()
assert.equal controller.action, 'edit'
controller.annotation.text = "this should be reverted"
controller.revert()
assert.equal controller.annotation.text, undefined
)
describe("validate()", ->
validate = require('../annotation').validate
it("returns undefined if value is not an object", ->
for value in [2, "foo", true, null]
assert.equal(validate(value), undefined)
)
it("returns the length if the value contains a non-empty tags array", ->
assert.equal(
validate({
tags: ["foo", "bar"],
permissions: {
read: ["group:__world__"]
},
target: [1, 2, 3]
}),
2)
)
it("returns the length if the value contains a non-empty text string", ->
assert.equal(
validate({
text: "foobar",
permissions: {
read: ["group:__world__"]
},
target: [1, 2, 3]
}),
6)
)
it("returns true for private highlights", ->
# A "highlight" is an annotation with a target but no text or tags.
# These are allowed as long as they're not public.
assert.equal(
validate({
permissions: {
read: ["acct:seanh@hypothes.is"]
},
target: [1, 2, 3]
}),
true
)
)
it("returns true for group highlights", ->
# A "highlight" is an annotation with a target but no text or tags.
# This validate() function allows these in groups.
# FIXME: This client behavior is wrong - the API does not allow shared
# highlights.
assert.equal(
validate({
permissions: {
read: ["group:foo"]
},
target: [1, 2, 3]
}),
true
)
)
it("returns false for public highlights", ->
# A "highlight" is an annotation with a target but no text or tags.
# Public highlights aren't allowed.
assert.equal(
validate({
text: undefined,
tags: undefined,
permissions: {
read: ["group:__world__"]
},
target: [1, 2, 3]
}),
false
)
)
it("handles values with no permissions", ->
# A "highlight" is an annotation with a target but no text or tags.
# These are allowed as long as they're not public.
assert.equal(
validate({
permissions: undefined,
target: [1, 2, 3]
}),
true
)
)
it("handles permissions objects with no read", ->
# A "highlight" is an annotation with a target but no text or tags.
# These are allowed as long as they're not public.
assert.equal(
validate({
permissions: {
read: undefined
},
target: [1, 2, 3]
}),
true
)
)
)
// Generated by CoffeeScript 1.7.1
(function() {
var events, inject, module, _ref,
__indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
_ref = angular.mock, module = _ref.module, inject = _ref.inject;
events = require('../../events');
describe('annotation', function() {
var $compile, $document, $element, $q, $rootScope, $scope, $timeout, $window, annotation, controller, createDirective, fakeAnnotationMapper, fakeAnnotationUI, fakeDocumentDomainFilter, fakeDocumentTitleFilter, fakeDrafts, fakeFeatures, fakeFlash, fakeGroups, fakeMomentFilter, fakePermissions, fakePersonaFilter, fakeSession, fakeStore, fakeTags, fakeTime, fakeUrlEncodeFilter, isolateScope, sandbox;
$compile = null;
$document = null;
$element = null;
$q = null;
$rootScope = null;
$scope = null;
$timeout = null;
$window = null;
annotation = null;
controller = null;
isolateScope = null;
fakeAnnotationMapper = null;
fakeAnnotationUI = null;
fakeDrafts = null;
fakeFeatures = null;
fakeFlash = null;
fakeGroups = null;
fakeMomentFilter = null;
fakePermissions = null;
fakePersonaFilter = null;
fakeDocumentTitleFilter = null;
fakeDocumentDomainFilter = null;
fakeSession = null;
fakeStore = null;
fakeTags = null;
fakeTime = null;
fakeUrlEncodeFilter = null;
sandbox = null;
createDirective = function() {
$element = angular.element('<div annotation="annotation">');
$compile($element)($scope);
$scope.$digest();
controller = $element.controller('annotation');
return isolateScope = $element.isolateScope();
};
before(function() {
return angular.module('h', []).directive('annotation', require('../annotation').directive);
});
beforeEach(module('h'));
beforeEach(module('h.templates'));
beforeEach(module(function($provide) {
sandbox = sinon.sandbox.create();
fakeAnnotationMapper = {
createAnnotation: sandbox.stub().returns({
permissions: {
read: ['acct:bill@localhost'],
update: ['acct:bill@localhost'],
destroy: ['acct:bill@localhost'],
admin: ['acct:bill@localhost']
}
}),
deleteAnnotation: sandbox.stub()
};
fakeAnnotationUI = {};
fakeDrafts = {
update: sandbox.stub(),
remove: sandbox.stub(),
get: sandbox.stub()
};
fakeFeatures = {
flagEnabled: sandbox.stub().returns(true)
};
fakeFlash = sandbox.stub();
fakeMomentFilter = sandbox.stub().returns('ages ago');
fakePermissions = {
isShared: sandbox.stub().returns(true),
isPrivate: sandbox.stub().returns(false),
permits: sandbox.stub().returns(true),
shared: sandbox.stub().returns({
read: ['everybody']
}),
"private": sandbox.stub().returns({
read: ['justme']
}),
"default": sandbox.stub().returns({
read: ['default']
}),
setDefault: sandbox.stub()
};
fakePersonaFilter = sandbox.stub().returnsArg(0);
fakeDocumentTitleFilter = function(arg) {
return '';
};
fakeDocumentDomainFilter = function(arg) {
return '';
};
fakeSession = {
state: {
userid: 'acct:bill@localhost'
}
};
fakeTags = {
filter: sandbox.stub().returns('a while ago'),
store: sandbox.stub()
};
fakeTime = {
toFuzzyString: sandbox.stub().returns('a while ago'),
nextFuzzyUpdate: sandbox.stub().returns(30)
};
fakeUrlEncodeFilter = function(v) {
return encodeURIComponent(v);
};
fakeGroups = {
focused: function() {
return {};
},
get: function() {}
};
$provide.value('annotationMapper', fakeAnnotationMapper);
$provide.value('annotationUI', fakeAnnotationUI);
$provide.value('drafts', fakeDrafts);
$provide.value('features', fakeFeatures);
$provide.value('flash', fakeFlash);
$provide.value('momentFilter', fakeMomentFilter);
$provide.value('permissions', fakePermissions);
$provide.value('personaFilter', fakePersonaFilter);
$provide.value('documentTitleFilter', fakeDocumentTitleFilter);
$provide.value('documentDomainFilter', fakeDocumentDomainFilter);
$provide.value('session', fakeSession);
$provide.value('store', fakeStore);
$provide.value('tags', fakeTags);
$provide.value('time', fakeTime);
$provide.value('urlencodeFilter', fakeUrlEncodeFilter);
$provide.value('groups', fakeGroups);
}));
beforeEach(inject(function(_$compile_, _$document_, _$q_, _$rootScope_, _$timeout_, _$window_) {
$compile = _$compile_;
$document = _$document_;
$window = _$window_;
$q = _$q_;
$timeout = _$timeout_;
$rootScope = _$rootScope_;
$scope = $rootScope.$new();
return $scope.annotation = annotation = {
id: 'deadbeef',
document: {
title: 'A special document'
},
target: [{}],
uri: 'http://example.com',
user: 'acct:bill@localhost'
};
}));
afterEach(function() {
return sandbox.restore();
});
describe('when the annotation is a highlight', function() {
beforeEach(function() {
annotation.$highlight = true;
return annotation.$create = sinon.stub().returns({
then: angular.noop,
"catch": angular.noop,
"finally": angular.noop
});
});
it('persists upon login', function() {
delete annotation.id;
delete annotation.user;
fakeSession.state.userid = null;
createDirective();
$scope.$digest();
assert.notCalled(annotation.$create);
fakeSession.state.userid = 'acct:ted@wyldstallyns.com';
$scope.$broadcast(events.USER_CHANGED, {});
$scope.$digest();
return assert.calledOnce(annotation.$create);
});
return it('is private', function() {
delete annotation.id;
createDirective();
$scope.$digest();
return assert.deepEqual(annotation.permissions, {
read: ['justme']
});
});
});
describe('#reply', function() {
var container;
container = null;
beforeEach(function() {
createDirective();
return annotation.permissions = {
read: ['acct:joe@localhost'],
update: ['acct:joe@localhost'],
destroy: ['acct:joe@localhost'],
admin: ['acct:joe@localhost']
};
});
it('creates a new reply with the proper uri and references', function() {
var match;
controller.reply();
match = sinon.match({
references: [annotation.id],
uri: annotation.uri
});
return assert.calledWith(fakeAnnotationMapper.createAnnotation, match);
});
it('makes the annotation shared if the parent is shared', function() {
var reply;
reply = {};
fakeAnnotationMapper.createAnnotation.returns(reply);
fakePermissions.isShared.returns(true);
controller.reply();
return assert.deepEqual(reply.permissions, {
read: ['everybody']
});
});
it('makes the annotation shared if the parent is shared', function() {
var reply;
$scope.annotation.group = "my group";
$scope.annotation.permissions = {
read: ["my group"]
};
reply = {};
fakeAnnotationMapper.createAnnotation.returns(reply);
fakePermissions.isShared = function(permissions, group) {
return __indexOf.call(permissions.read, group) >= 0;
};
fakePermissions.shared = function(group) {
return {
read: [group]
};
};
controller.reply();
return assert(__indexOf.call(reply.permissions.read, "my group") >= 0);
});
it('does not add the world readable principal if the parent is private', function() {
var reply;
reply = {};
fakeAnnotationMapper.createAnnotation.returns(reply);
fakePermissions.isShared.returns(false);
controller.reply();
return assert.deepEqual(reply.permissions, {
read: ['justme']
});
});
return it("sets the reply's group to be the same as its parent's", function() {
var reply;
$scope.annotation.group = "my group";
reply = {};
fakeAnnotationMapper.createAnnotation.returns(reply);
controller.reply();
return assert.equal(reply.group, $scope.annotation.group);
});
});
describe('#setPrivacy', function() {
beforeEach(function() {
return createDirective();
});
it('makes the annotation private when level is "private"', function() {
annotation.$update = sinon.stub().returns(Promise.resolve());
controller.edit();
controller.setPrivacy('private');
return controller.save().then(function() {
return assert.deepEqual(annotation.permissions, {
read: ['justme']
});
});
});
it('makes the annotation shared when level is "shared"', function() {
annotation.$update = sinon.stub().returns(Promise.resolve());
controller.edit();
controller.setPrivacy('shared');
return controller.save().then(function() {
return assert.deepEqual(annotation.permissions, {
read: ['everybody']
});
});
});
it('saves the "shared" visibility level to localStorage', function() {
annotation.$update = sinon.stub().returns(Promise.resolve());
controller.edit();
controller.setPrivacy('shared');
return controller.save().then(function() {
return assert(fakePermissions.setDefault.calledWithExactly('shared'));
});
});
it('saves the "private" visibility level to localStorage', function() {
annotation.$update = sinon.stub().returns(Promise.resolve());
controller.edit();
controller.setPrivacy('private');
return controller.save().then(function() {
return assert(fakePermissions.setDefault.calledWithExactly('private'));
});
});
return it("doesn't save the visibility if the annotation is a reply", function() {
annotation.$update = sinon.stub().returns(Promise.resolve());
annotation.references = ["parent id"];
controller.edit();
controller.setPrivacy('private');
return controller.save().then(function() {
return assert(!fakePermissions.setDefault.called);
});
});
});
describe('#hasContent', function() {
beforeEach(function() {
return createDirective();
});
it('returns false if the annotation has no tags or text', function() {
controller.annotation.text = '';
controller.annotation.tags = [];
return assert.ok(!controller.hasContent());
});
return it('returns true if the annotation has tags or text', function() {
controller.annotation.text = 'bar';
assert.ok(controller.hasContent());
controller.annotation.text = '';
controller.annotation.tags = [
{
text: 'foo'
}
];
return assert.ok(controller.hasContent());
});
});
describe('#hasQuotes', function() {
beforeEach(function() {
return createDirective();
});
it('returns false if the annotation has no quotes', function() {
controller.annotation.target = [{}];
return assert.isFalse(controller.hasQuotes());
});
return it('returns true if the annotation has quotes', function() {
controller.annotation.target = [
{
selector: [
{
type: 'TextQuoteSelector'
}
]
}
];
return assert.isTrue(controller.hasQuotes());
});
});
describe('#render', function() {
beforeEach(function() {
createDirective();
return sandbox.spy(controller, 'render');
});
afterEach(function() {
return sandbox.restore();
});
it('is called exactly once on model changes', function() {
assert.notCalled(controller.render);
annotation["delete"] = true;
$scope.$digest();
assert.calledOnce(controller.render);
annotation.booz = 'baz';
$scope.$digest();
return assert.calledTwice(controller.render);
});
it('provides a document title', function() {
controller.render();
return assert.equal(controller.document.title, 'A special document');
});
it('uses the first title when there are more than one', function() {
annotation.document.title = ['first title', 'second title'];
controller.render();
return assert.equal(controller.document.title, 'first title');
});
it('truncates long titles', function() {
annotation.document.title = 'A very very very long title that really\nshouldn\'t be found on a page on the internet.';
controller.render();
return assert.equal(controller.document.title, 'A very very very long title th…');
});
it('provides a document uri', function() {
controller.render();
return assert.equal(controller.document.uri, 'http://example.com');
});
it('provides an extracted domain from the uri', function() {
controller.render();
return assert.equal(controller.document.domain, 'example.com');
});
it('uses the domain for the title if the title is not present', function() {
delete annotation.document.title;
controller.render();
return assert.equal(controller.document.title, 'example.com');
});
it('still sets the uri correctly if the annotation has no document', function() {
delete annotation.document;
controller.render();
return assert(controller.document.uri === $scope.annotation.uri);
});
it('still sets the domain correctly if the annotation has no document', function() {
delete annotation.document;
controller.render();
return assert(controller.document.domain === 'example.com');
});
it('uses the domain for the title when the annotation has no document', function() {
delete annotation.document;
controller.render();
return assert(controller.document.title === 'example.com');
});
describe('timestamp', function() {
var clock;
clock = null;
beforeEach(function() {
clock = sinon.useFakeTimers();
annotation.created = (new Date()).toString();
return annotation.updated = (new Date()).toString();
});
afterEach(function() {
return clock.restore();
});
it('is not updated for unsaved annotations', function() {
annotation.updated = null;
$scope.$digest();
return assert.equal(controller.timestamp, null);
});
it('is updated on first digest', function() {
$scope.$digest();
return assert.equal(controller.timestamp, 'a while ago');
});
it('is updated after a timeout', function() {
fakeTime.nextFuzzyUpdate.returns(10);
fakeTime.toFuzzyString.returns('ages ago');
$scope.$digest();
clock.tick(11000);
$timeout.flush();
return assert.equal(controller.timestamp, 'ages ago');
});
return it('is no longer updated after the scope is destroyed', function() {
$scope.$digest();
$scope.$destroy();
$timeout.flush();
return $timeout.verifyNoPendingTasks();
});
});
return describe('share', function() {
var dialog;
dialog = null;
beforeEach(function() {
return dialog = $element.find('.share-dialog-wrapper');
});
return it('sets and unsets the open class on the share wrapper', function() {
dialog.find('button').click();
isolateScope.$digest();
assert.ok(dialog.hasClass('open'));
$document.click();
return assert.notOk(dialog.hasClass('open'));
});
});
});
describe("deleteAnnotation() method", function() {
beforeEach(function() {
createDirective();
fakeAnnotationMapper.deleteAnnotation = sandbox.stub();
return fakeFlash.error = sandbox.stub();
});
it("calls annotationMapper.delete() if the delete is confirmed", function(done) {
sandbox.stub($window, 'confirm').returns(true);
fakeAnnotationMapper.deleteAnnotation.returns($q.resolve());
controller["delete"]().then(function() {
assert(fakeAnnotationMapper.deleteAnnotation.calledWith(annotation));
return done();
});
return $timeout.flush();
});
it("doesn't call annotationMapper.delete() if the delete is cancelled", function() {
sandbox.stub($window, 'confirm').returns(false);
return assert(fakeAnnotationMapper.deleteAnnotation.notCalled);
});
it("flashes a generic error if the server cannot be reached", function(done) {
sandbox.stub($window, 'confirm').returns(true);
fakeAnnotationMapper.deleteAnnotation.returns($q.reject({
status: 0
}));
controller["delete"]().then(function() {
assert(fakeFlash.error.calledWith("Service unreachable.", "Deleting annotation failed"));
return done();
});
return $timeout.flush();
});
it("flashes an error if the delete fails on the server", function(done) {
sandbox.stub($window, 'confirm').returns(true);
fakeAnnotationMapper.deleteAnnotation.returns($q.reject({
status: 500,
statusText: "Server Error",
data: {}
}));
controller["delete"]().then(function() {
assert(fakeFlash.error.calledWith("500 Server Error", "Deleting annotation failed"));
return done();
});
return $timeout.flush();
});
return it("doesn't flash an error if the delete succeeds", function(done) {
sandbox.stub($window, 'confirm').returns(true);
fakeAnnotationMapper.deleteAnnotation.returns($q.resolve());
controller["delete"]().then(function() {
assert(fakeFlash.error.notCalled);
return done();
});
return $timeout.flush();
});
});
describe("saving a new annotation", function() {
beforeEach(function() {
createDirective();
fakeFlash.error = sandbox.stub();
controller.action = 'create';
return annotation.$create = sandbox.stub();
});
it("emits annotationCreated when saving an annotation succeeds", function(done) {
sandbox.spy($rootScope, '$emit');
annotation.$create.returns(Promise.resolve());
return controller.save().then(function() {
assert($rootScope.$emit.calledWith("annotationCreated"));
return done();
});
});
it("flashes a generic error if the server cannot be reached", function(done) {
annotation.$create.returns(Promise.reject({
status: 0
}));
controller.save().then(function() {
assert(fakeFlash.error.calledWith("Service unreachable.", "Saving annotation failed"));
return done();
});
return $timeout.flush();
});
it("flashes an error if saving the annotation fails on the server", function(done) {
annotation.$create.returns(Promise.reject({
status: 500,
statusText: "Server Error",
data: {}
}));
return controller.save().then(function() {
assert(fakeFlash.error.calledWith("500 Server Error", "Saving annotation failed"));
return done();
});
});
return it("doesn't flash an error when saving an annotation succeeds", function() {
annotation.$create.returns(Promise.resolve());
controller.save();
return assert(fakeFlash.error.notCalled);
});
});
describe("saving an edited an annotation", function() {
beforeEach(function() {
createDirective();
fakeFlash.error = sandbox.stub();
controller.action = 'edit';
return annotation.$update = sandbox.stub();
});
it("flashes a generic error if the server cannot be reached", function(done) {
annotation.$update.returns(Promise.reject({
status: 0
}));
return controller.save().then(function() {
assert(fakeFlash.error.calledWith("Service unreachable.", "Saving annotation failed"));
return done();
});
});
it("flashes an error if saving the annotation fails on the server", function(done) {
annotation.$update.returns(Promise.reject({
status: 500,
statusText: "Server Error",
data: {}
}));
return controller.save().then(function() {
assert(fakeFlash.error.calledWith("500 Server Error", "Saving annotation failed"));
return done();
});
});
return it("doesn't flash an error if saving the annotation succeeds", function() {
annotation.$update.returns(Promise.resolve());
controller.save();
return assert(fakeFlash.error.notCalled);
});
});
describe("drafts", function() {
it("creates a draft when editing an annotation", function() {
createDirective();
controller.edit();
return assert.calledWith(fakeDrafts.update, annotation, {
text: annotation.text,
tags: annotation.tags,
permissions: annotation.permissions
});
});
it("starts editing immediately if there is a draft", function() {
fakeDrafts.get.returns({
tags: [
{
text: 'unsaved'
}
],
text: 'unsaved-text'
});
createDirective();
return assert.isTrue(controller.editing);
});
it("uses the text and tags from the draft if present", function() {
fakeDrafts.get.returns({
tags: ['unsaved-tag'],
text: 'unsaved-text'
});
createDirective();
assert.deepEqual(controller.annotation.tags, [
{
text: 'unsaved-tag'
}
]);
return assert.equal(controller.annotation.text, 'unsaved-text');
});
it("removes the draft when changes are discarded", function() {
createDirective();
controller.edit();
controller.revert();
return assert.calledWith(fakeDrafts.remove, annotation);
});
return it("removes the draft when changes are saved", function() {
annotation.$update = sandbox.stub().returns(Promise.resolve());
createDirective();
controller.edit();
controller.save();
annotation.updated = (new Date()).toISOString();
$scope.$digest();
return assert.calledWith(fakeDrafts.remove, annotation);
});
});
describe("when the focused group changes", function() {
it("updates the current draft", function() {
createDirective();
controller.edit();
controller.annotation.text = 'unsaved-text';
controller.annotation.tags = [];
controller.annotation.permissions = 'new permissions';
fakeDrafts.get = sinon.stub().returns({
text: 'old-draft'
});
fakeDrafts.update = sinon.stub();
$rootScope.$broadcast(events.GROUP_FOCUSED);
return assert.calledWith(fakeDrafts.update, annotation, {
text: 'unsaved-text',
tags: [],
permissions: 'new permissions'
});
});
it("should not create a new draft", function() {
createDirective();
controller.edit();
fakeDrafts.update = sinon.stub();
fakeDrafts.get = sinon.stub().returns(null);
$rootScope.$broadcast(events.GROUP_FOCUSED);
return assert.notCalled(fakeDrafts.update);
});
return it("moves new annotations to the focused group", function() {
annotation.id = null;
createDirective();
fakeGroups.focused = sinon.stub().returns({
id: 'new-group'
});
$rootScope.$broadcast(events.GROUP_FOCUSED);
return assert.equal(annotation.group, 'new-group');
});
});
it("updates perms when moving new annotations to the focused group", function() {
annotation.id = null;
annotation.group = 'old-group';
annotation.permissions = {
read: [annotation.group]
};
fakePermissions.isShared.returns(true);
createDirective();
fakePermissions.shared = function(groupId) {
return {
read: [groupId]
};
};
fakeGroups.focused = sinon.stub().returns({
id: 'new-group'
});
$rootScope.$broadcast(events.GROUP_FOCUSED);
return assert.deepEqual(annotation.permissions.read, ['new-group']);
});
it("saves shared permissions for the new group to drafts", function() {
annotation.id = null;
annotation.group = 'old-group';
annotation.permissions = {
read: [annotation.group]
};
fakePermissions.isShared.returns(true);
createDirective();
fakeDrafts.get.returns(true);
fakePermissions.shared = function(groupId) {
return {
read: [groupId]
};
};
fakeGroups.focused = sinon.stub().returns({
id: 'new-group'
});
$rootScope.$broadcast(events.GROUP_FOCUSED);
return assert.deepEqual(fakeDrafts.update.lastCall.args[1].permissions.read, ['new-group'], "Shared permissions for the new group should be saved to drafts");
});
return it("does not change perms when moving new private annotations", function() {
annotation.id = null;
annotation.group = 'old-group';
annotation.permissions = {
read: ['acct:bill@localhost']
};
createDirective();
fakePermissions.isShared.returns(false);
fakeGroups.focused = sinon.stub().returns({
id: 'new-group'
});
$rootScope.$broadcast(events.GROUP_FOCUSED);
return assert.deepEqual(annotation.permissions.read, ['acct:bill@localhost'], 'The annotation should still be private');
});
});
describe("AnnotationController", function() {
var createAnnotationDirective, getCompileService, getRootScope;
before(function() {
return angular.module("h", []).directive("annotation", require("../annotation").directive);
});
beforeEach(module("h"));
beforeEach(module("h.templates"));
getCompileService = function() {
var $compile;
$compile = null;
inject(function(_$compile_) {
return $compile = _$compile_;
});
return $compile;
};
getRootScope = function() {
var $rootScope;
$rootScope = null;
inject(function(_$rootScope_) {
return $rootScope = _$rootScope_;
});
return $rootScope;
};
/*
Return an annotation directive instance and stub services etc.
*/
createAnnotationDirective = function(_arg) {
var annotation, annotationMapper, annotationUI, compiledElement, documentDomainFilter, documentTitleFilter, drafts, features, flash, groups, localStorage, locals, momentFilter, permissions, personaFilter, session, tags, time, urlencodeFilter;
annotation = _arg.annotation, personaFilter = _arg.personaFilter, momentFilter = _arg.momentFilter, urlencodeFilter = _arg.urlencodeFilter, drafts = _arg.drafts, features = _arg.features, flash = _arg.flash, permissions = _arg.permissions, session = _arg.session, tags = _arg.tags, time = _arg.time, annotationUI = _arg.annotationUI, annotationMapper = _arg.annotationMapper, groups = _arg.groups, documentTitleFilter = _arg.documentTitleFilter, documentDomainFilter = _arg.documentDomainFilter, localStorage = _arg.localStorage;
session = session || {
state: {
userid: "acct:fred@hypothes.is"
}
};
locals = {
personaFilter: personaFilter || function() {},
momentFilter: momentFilter || {},
urlencodeFilter: urlencodeFilter || {},
drafts: drafts || {
update: function() {},
remove: function() {},
get: function() {}
},
features: features || {
flagEnabled: function() {
return true;
}
},
flash: flash || {
info: function() {},
error: function() {}
},
permissions: permissions || {
isShared: function(permissions, group) {
return __indexOf.call(permissions.read, group) >= 0;
},
isPrivate: function(permissions, user) {
return __indexOf.call(permissions.read, user) >= 0;
},
permits: function() {
return true;
},
shared: function() {
return {};
},
"private": function() {
return {};
},
"default": function() {
return {};
},
setDefault: function() {}
},
session: session,
tags: tags || {
store: function() {}
},
time: time || {
toFuzzyString: function() {},
nextFuzzyUpdate: function() {}
},
annotationUI: annotationUI || {},
annotationMapper: annotationMapper || {},
groups: groups || {
get: function() {},
focused: function() {
return {};
}
},
documentTitleFilter: documentTitleFilter || function() {
return '';
},
documentDomainFilter: documentDomainFilter || function() {
return '';
},
localStorage: localStorage || {
setItem: function() {},
getItem: function() {}
}
};
module(function($provide) {
$provide.value("personaFilter", locals.personaFilter);
$provide.value("momentFilter", locals.momentFilter);
$provide.value("urlencodeFilter", locals.urlencodeFilter);
$provide.value("drafts", locals.drafts);
$provide.value("features", locals.features);
$provide.value("flash", locals.flash);
$provide.value("permissions", locals.permissions);
$provide.value("session", locals.session);
$provide.value("tags", locals.tags);
$provide.value("time", locals.time);
$provide.value("annotationUI", locals.annotationUI);
$provide.value("annotationMapper", locals.annotationMapper);
$provide.value("groups", locals.groups);
$provide.value("documentTitleFilter", locals.documentTitleFilter);
$provide.value("documentDomainFilter", locals.documentDomainFilter);
$provide.value("localStorage", locals.localStorage);
});
locals.element = angular.element('<div annotation="annotation">');
compiledElement = getCompileService()(locals.element);
locals.$rootScope = getRootScope();
locals.parentScope = locals.$rootScope.$new();
locals.parentScope.annotation = annotation || {};
locals.directive = compiledElement(locals.parentScope);
locals.$rootScope.$digest();
locals.controller = locals.element.controller("annotation");
locals.isolateScope = locals.element.isolateScope();
return locals;
};
describe("createAnnotationDirective", function() {
return it("creates the directive without crashing", function() {
return createAnnotationDirective({});
});
});
it("sets the user of new annotations", function() {
var annotation, session;
annotation = {};
session = createAnnotationDirective({
annotation: annotation
}).session;
return assert.equal(annotation.user, session.state.userid);
});
it("sets the permissions of new annotations", function() {
var annotation, permissions;
annotation = {
group: "test-group"
};
permissions = {
"default": sinon.stub().returns("default permissions"),
isShared: function() {},
isPrivate: function() {}
};
createAnnotationDirective({
annotation: annotation,
permissions: permissions
});
assert(permissions["default"].calledWithExactly("test-group"));
return assert.equal(annotation.permissions, "default permissions", "It should set a new annotation's permissions to what permissions.default() returns");
});
it("doesn't overwrite permissions if the annotation already has them", function() {
var annotation, original_permissions, permissions;
annotation = {
permissions: {
read: ["foo"],
update: ["bar"],
"delete": ["gar"],
admin: ["har"]
}
};
original_permissions = JSON.parse(JSON.stringify(annotation.permissions));
permissions = {
"default": sinon.stub().returns("new permissions"),
isShared: function() {},
isPrivate: function() {}
};
createAnnotationDirective({
annotation: annotation,
permissions: permissions
});
return assert.deepEqual(annotation.permissions, original_permissions);
});
describe("save", function() {
return it("Passes group:<id> to the server when saving a new annotation", function() {
var annotation, controller, group;
annotation = {
user: 'acct:fred@hypothes.is',
text: 'foo'
};
annotation.$create = sinon.stub().returns(Promise.resolve());
group = {
id: "test-id"
};
controller = createAnnotationDirective({
annotation: annotation,
groups: {
focused: function() {
return group;
},
get: function() {}
}
}).controller;
controller.action = 'create';
return controller.save().then(function() {
return assert(annotation.$create.lastCall.thisValue.group === "test-id");
});
});
});
describe("when the user signs in", function() {
it("sets the user of unsaved annotations", function() {
var $rootScope, annotation, session;
annotation = {};
session = {
state: {
userid: null
}
};
$rootScope = createAnnotationDirective({
annotation: annotation,
session: session
}).$rootScope;
assert(!annotation.user);
session.state.userid = "acct:fred@hypothes.is";
$rootScope.$broadcast(events.USER_CHANGED, {});
return assert.equal(annotation.user, session.state.userid);
});
return it("sets the permissions of unsaved annotations", function() {
var $rootScope, annotation, permissions, session;
annotation = {
group: "__world__"
};
session = {
state: {
userid: null
}
};
permissions = {
"default": function() {
return null;
},
isShared: function() {},
isPrivate: function() {}
};
$rootScope = createAnnotationDirective({
annotation: annotation,
session: session,
permissions: permissions
}).$rootScope;
assert(!annotation.permissions);
session.state.userid = "acct:fred@hypothes.is";
permissions["default"] = function() {
return "__default_permissions__";
};
$rootScope.$broadcast(events.USER_CHANGED, {});
return assert.equal(annotation.permissions, "__default_permissions__");
});
});
/*
Simulate what happens when the user edits an annotation, clicks Save,
gets an error because the server fails to save the annotation, then clicks
Cancel - in the frontend the annotation should be restored to its original
value, the edits lost.
*/
return it("restores the original text when editing is cancelled", function() {
var controller, original_text;
controller = createAnnotationDirective({
annotation: {
id: "test-annotation-id",
user: "acct:bill@localhost",
text: "Initial annotation body text",
$create: function() {
return Promise.resolve();
},
$update: function() {
return Promise.reject({
status: 500,
statusText: "Server Error",
data: {}
});
}
}
}).controller;
original_text = controller.annotation.text;
controller.edit();
controller.annotation.text = "changed by test code";
controller.save();
assert(controller.annotation.text === "changed by test code");
controller.revert();
assert(controller.annotation.text === original_text);
return it("clears the text when reverting changes to a highlight", function() {
controller = createAnnotationDirective({
annotation: {
id: "test-annotation-id",
user: "acct:bill@localhost"
}
}).controller;
controller.edit();
assert.equal(controller.action, 'edit');
controller.annotation.text = "this should be reverted";
controller.revert();
return assert.equal(controller.annotation.text, void 0);
});
});
});
describe("validate()", function() {
var validate;
validate = require('../annotation').validate;
it("returns undefined if value is not an object", function() {
var value, _i, _len, _ref1, _results;
_ref1 = [2, "foo", true, null];
_results = [];
for (_i = 0, _len = _ref1.length; _i < _len; _i++) {
value = _ref1[_i];
_results.push(assert.equal(validate(value), void 0));
}
return _results;
});
it("returns the length if the value contains a non-empty tags array", function() {
return assert.equal(validate({
tags: ["foo", "bar"],
permissions: {
read: ["group:__world__"]
},
target: [1, 2, 3]
}), 2);
});
it("returns the length if the value contains a non-empty text string", function() {
return assert.equal(validate({
text: "foobar",
permissions: {
read: ["group:__world__"]
},
target: [1, 2, 3]
}), 6);
});
it("returns true for private highlights", function() {
return assert.equal(validate({
permissions: {
read: ["acct:seanh@hypothes.is"]
},
target: [1, 2, 3]
}), true);
});
it("returns true for group highlights", function() {
return assert.equal(validate({
permissions: {
read: ["group:foo"]
},
target: [1, 2, 3]
}), true);
});
it("returns false for public highlights", function() {
return assert.equal(validate({
text: void 0,
tags: void 0,
permissions: {
read: ["group:__world__"]
},
target: [1, 2, 3]
}), false);
});
it("handles values with no permissions", function() {
return assert.equal(validate({
permissions: void 0,
target: [1, 2, 3]
}), true);
});
return it("handles permissions objects with no read", function() {
return assert.equal(validate({
permissions: {
read: void 0
},
target: [1, 2, 3]
}), true);
});
});
}).call(this);
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