Commit 58d19bf0 authored by Robert Knight's avatar Robert Knight

Merge pull request #2762 from hypothesis/2728-refactor-highlight-saving

Refactor saving highlights in AnnotationController
parents 696909f7 6b10b936
...@@ -186,7 +186,81 @@ function AnnotationController( ...@@ -186,7 +186,81 @@ function AnnotationController(
// Set the permissions of new annotations. // Set the permissions of new annotations.
model.permissions = model.permissions || permissions['default'](model.group); model.permissions = model.permissions || permissions['default'](model.group);
var highlight = model.$highlight; /**
* `true` if this AnnotationController instance was created as a result of
* the highlight button being clicked.
*
* `false` if the annotation button was clicked, or if this is a highlight or
* annotation that was fetched from the server (as opposed to created new
* client-side).
*/
var newlyCreatedByHighlightButton = model.$highlight || false;
/**
* @ngdoc method
* @name annotation.AnnotationController#isHighlight.
* @returns {boolean} true if the annotation is a highlight, false otherwise
*/
vm.isHighlight = function() {
if (newlyCreatedByHighlightButton) {
return true;
} else if (!model.id) {
// If an annotation has no model.id (i.e. it has not been saved to the
// server yet) and newlyCreatedByHighlightButton is false, then it must
// be an annotation not a highlight (even though it may not have any
// text or tags yet).
return false;
} else {
// Once an annotation has been saved to the server there's no longer a
// simple property that says whether it's a highlight or not. For
// example there's no model.highlight: true. Instead a highlight is
// defined as an annotation that isn't a page note or a reply and that
// has no text or tags.
var isPageNote = (model.target || []).length === 0;
var isReply = (model.references || []).length !== 0;
return (!isPageNote && !isReply && !vm.hasContent());
}
};
/** Save this annotation if it's a new highlight.
*
* The highlight will be saved to the server if the user is logged in,
* saved to drafts if they aren't.
*
* If the annotation is not new (it has already been saved to the server) or
* is not a highlight then nothing will happen.
*
*/
function saveNewHighlight() {
if (model.id) {
// Already saved.
return;
}
if (!vm.isHighlight()) {
// Not a highlight,
return;
}
if (model.user) {
// User is logged in, save to server.
// Highlights are always private.
model.permissions = permissions.private();
model.$create().then(function() {
$rootScope.$emit('annotationCreated', model);
});
} else {
// User isn't logged in, save to drafts.
updateDraft(model);
}
}
// Automatically save new highlights to the server when they're created.
// Note that this line also gets called when the user logs in (since
// AnnotationController instances are re-created on login) so serves to
// automatically save highlights that were created while logged out when you
// log in.
saveNewHighlight();
/** /**
* @ngdoc method * @ngdoc method
...@@ -229,18 +303,6 @@ function AnnotationController( ...@@ -229,18 +303,6 @@ function AnnotationController(
return $q.when(tags.filter(query)); return $q.when(tags.filter(query));
}; };
/**
* @ngdoc method
* @name annotation.AnnotationController#isHighlight.
* @returns {boolean} True if the annotation is a highlight.
*/
vm.isHighlight = function() {
var targetLength = (model.target || []).length;
var referencesLength = (model.references || []).length;
var tagsLength = (model.tags || []).length;
return (targetLength && !referencesLength && !(model.text || tagsLength));
};
/** /**
* @ngdoc method * @ngdoc method
* @name annotation.AnnotationController#isPrivate * @name annotation.AnnotationController#isPrivate
...@@ -302,8 +364,8 @@ function AnnotationController( ...@@ -302,8 +364,8 @@ function AnnotationController(
/** /**
* @ngdoc method * @ngdoc method
* @name annotation.AnnotaitonController#hasContent * @name annotation.AnnotaitonController#hasContent
* @returns {boolean} True if the currently edited annotation has * @returns {boolean} `true` if this annotation has content, `false`
* content (ie. is not just a highlight) * otherwise.
*/ */
vm.hasContent = function() { vm.hasContent = function() {
var textLength = (vm.annotation.text || '').length; var textLength = (vm.annotation.text || '').length;
...@@ -573,19 +635,6 @@ function AnnotationController( ...@@ -573,19 +635,6 @@ function AnnotationController(
drafts.remove(model); drafts.remove(model);
} }
// Save highlights once logged in.
if (vm.isHighlight() && highlight) {
if (model.user && !model.id) {
model.permissions = permissions.private();
model.$create().then(function() {
$rootScope.$emit('annotationCreated', model);
});
highlight = false; // Prevents double highlight creation.
} else {
updateDraft(model);
}
}
updateTimestamp(model === old); // Repeat on first run. updateTimestamp(model === old); // Repeat on first run.
vm.render(); vm.render();
}, true); }, true);
...@@ -604,12 +653,14 @@ function AnnotationController( ...@@ -604,12 +653,14 @@ function AnnotationController(
} }
}); });
// If this is a new annotation or we have unsaved changes, // If this annotation is not a highlight and if it's new (has just been
// then start editing immediately. // created by the annotate button) or it has edits not yet saved to the
var isNewAnnotation = !(model.id || (vm.isHighlight() && highlight)); // server - then open the editor on AnnotationController instantiation.
if (isNewAnnotation || drafts.get(model)) { if (!newlyCreatedByHighlightButton) {
if (!model.id || drafts.get(model)) {
vm.edit(); vm.edit();
} }
}
// When the current group changes, persist any unsaved changes using // When the current group changes, persist any unsaved changes using
// the drafts service. They will be restored when this annotation is // the drafts service. They will be restored when this annotation is
......
...@@ -303,7 +303,6 @@ describe('annotation.js', function() { ...@@ -303,7 +303,6 @@ describe('annotation.js', function() {
var $scope; var $scope;
var $timeout; var $timeout;
var $window; var $window;
var annotation;
var fakeAnnotationMapper; var fakeAnnotationMapper;
var fakeAnnotationUI; var fakeAnnotationUI;
var fakeDocumentDomainFilter; var fakeDocumentDomainFilter;
...@@ -321,19 +320,118 @@ describe('annotation.js', function() { ...@@ -321,19 +320,118 @@ describe('annotation.js', function() {
var fakeUrlEncodeFilter; var fakeUrlEncodeFilter;
var sandbox; var sandbox;
function createDirective() { function createDirective(annotation) {
annotation = annotation || defaultAnnotation();
$scope.annotation = annotation;
var element = angular.element('<div annotation="annotation">'); var element = angular.element('<div annotation="annotation">');
compileService()(element)($scope); compileService()(element)($scope);
$scope.$digest(); $scope.$digest();
var controller = element.controller('annotation'); var controller = element.controller('annotation');
var scope = element.isolateScope(); var scope = element.isolateScope();
return { return {
annotation: annotation,
controller: controller, controller: controller,
element: element, element: element,
scope: scope scope: scope
}; };
} }
/** Return the default domain model object that createDirective() uses if
* no custom one is passed to it. */
function defaultAnnotation() {
return {
id: 'deadbeef',
document: {
title: 'A special document'
},
target: [{}],
uri: 'http://example.com',
user: 'acct:bill@localhost'
};
}
/** Return an annotation domain model object for a new annotation
* (newly-created client-side, not yet saved to the server).
*/
function newAnnotation() {
// A new annotation won't have any saved drafts yet.
fakeDrafts.get.returns(null);
return {
id: undefined,
$highlight: undefined,
target: ['foo', 'bar'],
references: [],
text: 'Annotation text',
tags: ['tag_1', 'tag_2']
};
}
/** Return an annotation domain model object for a new highlight
* (newly-created client-side, not yet saved to the server).
*/
function newHighlight() {
// A new highlight won't have any saved drafts yet.
fakeDrafts.get.returns(null);
return {
id: undefined,
$highlight: true
};
}
/** Return an annotation domain model object for an existing annotation
* received from the server.
*/
function oldAnnotation() {
return {
id: 'annotation_id',
$highlight: undefined,
target: ['foo', 'bar'],
references: [],
text: 'This is my annotation',
tags: ['tag_1', 'tag_2']
};
}
/** Return an annotation domain model object for an existing highlight
* received from the server.
*/
function oldHighlight() {
return {
id: 'annotation_id',
$highlight: undefined,
target: ['foo', 'bar'],
references: [],
text: '',
tags: []
};
}
/** Return an annotation domain model object for an existing page note
* received from the server.
*/
function oldPageNote() {
return {
highlight: undefined,
target: [],
references: [],
text: '',
tags: []
};
}
/** Return an annotation domain model object for an existing reply
* received from the server.
*/
function oldReply() {
return {
highlight: undefined,
target: ['foo'],
references: ['parent_annotation_id'],
text: '',
tags: []
};
}
before(function() { before(function() {
angular.module('h', []) angular.module('h', [])
.directive('annotation', require('../annotation').directive); .directive('annotation', require('../annotation').directive);
...@@ -453,15 +551,6 @@ describe('annotation.js', function() { ...@@ -453,15 +551,6 @@ describe('annotation.js', function() {
$timeout = _$timeout_; $timeout = _$timeout_;
$rootScope = _$rootScope_; $rootScope = _$rootScope_;
$scope = $rootScope.$new(); $scope = $rootScope.$new();
$scope.annotation = annotation = {
id: 'deadbeef',
document: {
title: 'A special document'
},
target: [{}],
uri: 'http://example.com',
user: 'acct:bill@localhost'
};
} }
) )
); );
...@@ -470,6 +559,112 @@ describe('annotation.js', function() { ...@@ -470,6 +559,112 @@ describe('annotation.js', function() {
sandbox.restore(); sandbox.restore();
}); });
describe('AnnotationController() initialization', function() {
it('saves new highlights to the server on initialization', function() {
var annotation = newHighlight();
// The user is logged-in.
annotation.user = fakeSession.state.userid = 'acct:bill@localhost';
annotation.$create = sandbox.stub().returns({
then: function() {}
});
createDirective(annotation);
assert.called(annotation.$create);
});
it('saves new highlights to drafts if not logged in', function() {
var annotation = newHighlight();
// The user is not logged-in.
annotation.user = fakeSession.state.userid = undefined;
annotation.$create = sandbox.stub().returns({
then: function() {}
});
createDirective(annotation);
assert.notCalled(annotation.$create);
assert.called(fakeDrafts.update);
});
it('does not save new annotations on initialization', function() {
var annotation = newAnnotation();
annotation.$create = sandbox.stub().returns({
then: function() {}
});
createDirective(annotation);
assert.notCalled(annotation.$create);
});
it('does not save old highlights on initialization', function() {
var annotation = oldHighlight();
annotation.$create = sandbox.stub().returns({
then: function() {}
});
createDirective(annotation);
assert.notCalled(annotation.$create);
});
it('does not save old annotations on initialization', function() {
var annotation = oldAnnotation();
annotation.$create = sandbox.stub().returns({
then: function() {}
});
createDirective(annotation);
assert.notCalled(annotation.$create);
});
it('edits new annotations on initialization', function() {
var annotation = newAnnotation();
var controller = createDirective(annotation).controller;
assert.isTrue(controller.editing());
});
it('edits annotations with drafts on initialization', function() {
var annotation = oldAnnotation();
// The drafts service has some draft changes for this annotation.
fakeDrafts.get.returns('foo');
var controller = createDirective(annotation).controller;
assert.isTrue(controller.editing());
});
it('does not edit new highlights on initialization', function() {
var annotation = newHighlight();
// We have to set annotation.$create() because it'll try to call it.
annotation.$create = sandbox.stub().returns({
then: function() {}
});
var controller = createDirective(annotation).controller;
assert.isFalse(controller.editing());
});
it('edits highlights with drafts on initialization', function() {
var annotation = oldHighlight();
// You can edit a highlight, enter some text or tags, and save it (the
// highlight then becomes an annotation). You can also edit a highlight
// and then change focus to another group and back without saving the
// highlight, in which case the highlight will have draft edits.
// This highlight has draft edits.
fakeDrafts.get.returns('foo');
var controller = createDirective(annotation).controller;
assert.isTrue(controller.editing());
});
});
describe('AnnotationController.editing()', function() { describe('AnnotationController.editing()', function() {
it('returns true if action is "create"', function() { it('returns true if action is "create"', function() {
var controller = createDirective().controller; var controller = createDirective().controller;
...@@ -490,8 +685,77 @@ describe('annotation.js', function() { ...@@ -490,8 +685,77 @@ describe('annotation.js', function() {
}); });
}); });
describe('AnnotationController.isHighlight()', function() {
it('returns true for new highlights', function() {
var annotation = newHighlight();
// We need to define $create because it'll try to call it.
annotation.$create = function() {return {then: function() {}};};
var vm = createDirective(annotation).controller;
assert.isTrue(vm.isHighlight());
});
it('returns false for new annotations', function() {
var annotation = newAnnotation();
var vm = createDirective(annotation).controller;
assert.isFalse(vm.isHighlight());
});
it('returns false for page notes', function() {
var annotation = oldPageNote();
var vm = createDirective(annotation).controller;
assert.isFalse(vm.isHighlight());
});
it('returns false for replies', function() {
var annotation = oldReply();
var vm = createDirective(annotation).controller;
assert.isFalse(vm.isHighlight());
});
it('returns false for annotations with text but no tags', function() {
var annotation = oldAnnotation();
annotation.text = 'This is my annotation';
annotation.tags = [];
var vm = createDirective(annotation).controller;
assert.isFalse(vm.isHighlight());
});
it('returns false for annotations with tags but no text', function() {
var annotation = oldAnnotation();
annotation.text = '';
annotation.tags = ['foo'];
var vm = createDirective(annotation).controller;
assert.isFalse(vm.isHighlight());
});
it('returns true for annotations with no text or tags', function() {
var annotation = oldAnnotation();
annotation.text = '';
annotation.tags = [];
var vm = createDirective(annotation).controller;
assert.isTrue(vm.isHighlight());
});
});
describe('when the annotation is a highlight', function() { describe('when the annotation is a highlight', function() {
var annotation;
beforeEach(function() { beforeEach(function() {
annotation = defaultAnnotation();
annotation.$highlight = true; annotation.$highlight = true;
annotation.$create = sinon.stub().returns({ annotation.$create = sinon.stub().returns({
then: angular.noop, then: angular.noop,
...@@ -500,22 +764,9 @@ describe('annotation.js', function() { ...@@ -500,22 +764,9 @@ describe('annotation.js', function() {
}); });
}); });
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();
assert.calledOnce(annotation.$create);
});
it('is private', function() { it('is private', function() {
delete annotation.id; delete annotation.id;
createDirective(); createDirective(annotation);
$scope.$digest(); $scope.$digest();
assert.deepEqual(annotation.permissions, { assert.deepEqual(annotation.permissions, {
read: ['justme'] read: ['justme']
...@@ -524,7 +775,10 @@ describe('annotation.js', function() { ...@@ -524,7 +775,10 @@ describe('annotation.js', function() {
}); });
describe('#reply', function() { describe('#reply', function() {
var annotation;
beforeEach(function() { beforeEach(function() {
annotation = defaultAnnotation();
annotation.permissions = { annotation.permissions = {
read: ['acct:joe@localhost'], read: ['acct:joe@localhost'],
update: ['acct:joe@localhost'], update: ['acct:joe@localhost'],
...@@ -534,7 +788,7 @@ describe('annotation.js', function() { ...@@ -534,7 +788,7 @@ describe('annotation.js', function() {
}); });
it('creates a new reply with the proper uri and references', function() { it('creates a new reply with the proper uri and references', function() {
var controller = createDirective().controller; var controller = createDirective(annotation).controller;
controller.reply(); controller.reply();
var match = sinon.match({ var match = sinon.match({
references: [annotation.id], references: [annotation.id],
...@@ -544,7 +798,7 @@ describe('annotation.js', function() { ...@@ -544,7 +798,7 @@ describe('annotation.js', function() {
}); });
it('makes the annotation shared if the parent is shared', function() { it('makes the annotation shared if the parent is shared', function() {
var controller = createDirective().controller; var controller = createDirective(annotation).controller;
var reply = {}; var reply = {};
fakeAnnotationMapper.createAnnotation.returns(reply); fakeAnnotationMapper.createAnnotation.returns(reply);
fakePermissions.isShared.returns(true); fakePermissions.isShared.returns(true);
...@@ -555,7 +809,7 @@ describe('annotation.js', function() { ...@@ -555,7 +809,7 @@ describe('annotation.js', function() {
}); });
it('makes the annotation shared if the parent is shared', function() { it('makes the annotation shared if the parent is shared', function() {
var controller = createDirective().controller; var controller = createDirective(annotation).controller;
$scope.annotation.group = 'my group'; $scope.annotation.group = 'my group';
$scope.annotation.permissions = { $scope.annotation.permissions = {
read: ['my group'] read: ['my group']
...@@ -577,7 +831,7 @@ describe('annotation.js', function() { ...@@ -577,7 +831,7 @@ describe('annotation.js', function() {
it( it(
'does not add the world readable principal if the parent is private', 'does not add the world readable principal if the parent is private',
function() { function() {
var controller = createDirective().controller; var controller = createDirective(annotation).controller;
var reply = {}; var reply = {};
fakeAnnotationMapper.createAnnotation.returns(reply); fakeAnnotationMapper.createAnnotation.returns(reply);
fakePermissions.isShared.returns(false); fakePermissions.isShared.returns(false);
...@@ -589,7 +843,7 @@ describe('annotation.js', function() { ...@@ -589,7 +843,7 @@ describe('annotation.js', function() {
); );
it('sets the reply\'s group to be the same as its parent\'s', function() { it('sets the reply\'s group to be the same as its parent\'s', function() {
var controller = createDirective().controller; var controller = createDirective(annotation).controller;
$scope.annotation.group = 'my group'; $scope.annotation.group = 'my group';
var reply = {}; var reply = {};
fakeAnnotationMapper.createAnnotation.returns(reply); fakeAnnotationMapper.createAnnotation.returns(reply);
...@@ -600,58 +854,58 @@ describe('annotation.js', function() { ...@@ -600,58 +854,58 @@ describe('annotation.js', function() {
describe('#setPrivacy', function() { describe('#setPrivacy', function() {
it('makes the annotation private when level is "private"', function() { it('makes the annotation private when level is "private"', function() {
var controller = createDirective().controller; var parts = createDirective();
annotation.$update = sinon.stub().returns(Promise.resolve()); parts.annotation.$update = sinon.stub().returns(Promise.resolve());
controller.edit(); parts.controller.edit();
controller.setPrivacy('private'); parts.controller.setPrivacy('private');
return controller.save().then(function() { return parts.controller.save().then(function() {
// Verify that the permissions are updated once the annotation // Verify that the permissions are updated once the annotation
// is saved. // is saved.
assert.deepEqual(annotation.permissions, { assert.deepEqual(parts.annotation.permissions, {
read: ['justme'] read: ['justme']
}); });
}); });
}); });
it('makes the annotation shared when level is "shared"', function() { it('makes the annotation shared when level is "shared"', function() {
var controller = createDirective().controller; var parts = createDirective();
annotation.$update = sinon.stub().returns(Promise.resolve()); parts.annotation.$update = sinon.stub().returns(Promise.resolve());
controller.edit(); parts.controller.edit();
controller.setPrivacy('shared'); parts.controller.setPrivacy('shared');
return controller.save().then(function() { return parts.controller.save().then(function() {
assert.deepEqual(annotation.permissions, { assert.deepEqual(parts.annotation.permissions, {
read: ['everybody'] read: ['everybody']
}); });
}); });
}); });
it('saves the "shared" visibility level to localStorage', function() { it('saves the "shared" visibility level to localStorage', function() {
var controller = createDirective().controller; var parts = createDirective();
annotation.$update = sinon.stub().returns(Promise.resolve()); parts.annotation.$update = sinon.stub().returns(Promise.resolve());
controller.edit(); parts.controller.edit();
controller.setPrivacy('shared'); parts.controller.setPrivacy('shared');
return controller.save().then(function() { return parts.controller.save().then(function() {
assert(fakePermissions.setDefault.calledWithExactly('shared')); assert(fakePermissions.setDefault.calledWithExactly('shared'));
}); });
}); });
it('saves the "private" visibility level to localStorage', function() { it('saves the "private" visibility level to localStorage', function() {
var controller = createDirective().controller; var parts = createDirective();
annotation.$update = sinon.stub().returns(Promise.resolve()); parts.annotation.$update = sinon.stub().returns(Promise.resolve());
controller.edit(); parts.controller.edit();
controller.setPrivacy('private'); parts.controller.setPrivacy('private');
return controller.save().then(function() { return parts.controller.save().then(function() {
assert(fakePermissions.setDefault.calledWithExactly('private')); assert(fakePermissions.setDefault.calledWithExactly('private'));
}); });
}); });
it('doesn\'t save the visibility if the annotation is a reply', function() { it('doesn\'t save the visibility if the annotation is a reply', function() {
var controller = createDirective().controller; var parts = createDirective();
annotation.$update = sinon.stub().returns(Promise.resolve()); parts.annotation.$update = sinon.stub().returns(Promise.resolve());
annotation.references = ['parent id']; parts.annotation.references = ['parent id'];
controller.edit(); parts.controller.edit();
controller.setPrivacy('private'); parts.controller.setPrivacy('private');
return controller.save().then(function() { return parts.controller.save().then(function() {
assert(!fakePermissions.setDefault.called); assert(!fakePermissions.setDefault.called);
}); });
}); });
...@@ -707,15 +961,15 @@ describe('annotation.js', function() { ...@@ -707,15 +961,15 @@ describe('annotation.js', function() {
}); });
it('is called exactly once on model changes', function() { it('is called exactly once on model changes', function() {
var controller = createDirective().controller; var parts = createDirective();
sandbox.spy(controller, 'render'); sandbox.spy(parts.controller, 'render');
assert.notCalled(controller.render); assert.notCalled(parts.controller.render);
annotation['delete'] = true; parts.annotation['delete'] = true;
$scope.$digest(); $scope.$digest();
assert.calledOnce(controller.render); assert.calledOnce(parts.controller.render);
annotation.booz = 'baz'; parts.annotation.booz = 'baz';
$scope.$digest(); $scope.$digest();
assert.calledTwice(controller.render); assert.calledTwice(parts.controller.render);
}); });
it('provides a document title', function() { it('provides a document title', function() {
...@@ -725,18 +979,18 @@ describe('annotation.js', function() { ...@@ -725,18 +979,18 @@ describe('annotation.js', function() {
}); });
it('uses the first title when there are more than one', function() { it('uses the first title when there are more than one', function() {
var controller = createDirective().controller; var parts = createDirective();
annotation.document.title = ['first title', 'second title']; parts.annotation.document.title = ['first title', 'second title'];
controller.render(); parts.controller.render();
assert.equal(controller.document.title, 'first title'); assert.equal(parts.controller.document.title, 'first title');
}); });
it('truncates long titles', function() { it('truncates long titles', function() {
var controller = createDirective().controller; var parts = createDirective();
annotation.document.title = 'A very very very long title that really\nshouldn\'t be found on a page on the internet.'; parts.annotation.document.title = 'A very very very long title that really\nshouldn\'t be found on a page on the internet.';
controller.render(); parts.controller.render();
assert.equal( assert.equal(
controller.document.title, 'A very very very long title th…'); parts.controller.document.title, 'A very very very long title th…');
}); });
it('provides a document uri', function() { it('provides a document uri', function() {
...@@ -752,47 +1006,49 @@ describe('annotation.js', function() { ...@@ -752,47 +1006,49 @@ describe('annotation.js', function() {
}); });
it('uses the domain for the title if the title is not present', function() { it('uses the domain for the title if the title is not present', function() {
var controller = createDirective().controller; var parts = createDirective();
delete annotation.document.title; delete parts.annotation.document.title;
controller.render(); parts.controller.render();
assert.equal(controller.document.title, 'example.com'); assert.equal(parts.controller.document.title, 'example.com');
}); });
it( it(
'still sets the uri correctly if the annotation has no document', 'still sets the uri correctly if the annotation has no document',
function() { function() {
var controller = createDirective().controller; var parts = createDirective();
delete annotation.document; delete parts.annotation.document;
controller.render(); parts.controller.render();
assert(controller.document.uri === $scope.annotation.uri); assert(parts.controller.document.uri === $scope.annotation.uri);
} }
); );
it( it(
'still sets the domain correctly if the annotation has no document', 'still sets the domain correctly if the annotation has no document',
function() { function() {
var controller = createDirective().controller; var parts = createDirective();
delete annotation.document; delete parts.annotation.document;
controller.render(); parts.controller.render();
assert(controller.document.domain === 'example.com'); assert(parts.controller.document.domain === 'example.com');
} }
); );
it( it(
'uses the domain for the title when the annotation has no document', 'uses the domain for the title when the annotation has no document',
function() { function() {
var controller = createDirective().controller; var parts = createDirective();
delete annotation.document; delete parts.annotation.document;
controller.render(); parts.controller.render();
assert(controller.document.title === 'example.com'); assert(parts.controller.document.title === 'example.com');
} }
); );
describe('timestamp', function() { describe('timestamp', function() {
var annotation;
var clock; var clock;
beforeEach(function() { beforeEach(function() {
clock = sinon.useFakeTimers(); clock = sinon.useFakeTimers();
annotation = defaultAnnotation();
annotation.created = (new Date()).toString(); annotation.created = (new Date()).toString();
annotation.updated = (new Date()).toString(); annotation.updated = (new Date()).toString();
}); });
...@@ -803,7 +1059,7 @@ describe('annotation.js', function() { ...@@ -803,7 +1059,7 @@ describe('annotation.js', function() {
it('is not updated for unsaved annotations', function() { it('is not updated for unsaved annotations', function() {
annotation.updated = null; annotation.updated = null;
var controller = createDirective().controller; var controller = createDirective(annotation).controller;
// Unsaved annotations don't have an updated time yet so a timestamp // Unsaved annotations don't have an updated time yet so a timestamp
// string can't be computed for them. // string can't be computed for them.
$scope.$digest(); $scope.$digest();
...@@ -811,13 +1067,13 @@ describe('annotation.js', function() { ...@@ -811,13 +1067,13 @@ describe('annotation.js', function() {
}); });
it('is updated on first digest', function() { it('is updated on first digest', function() {
var controller = createDirective().controller; var controller = createDirective(annotation).controller;
$scope.$digest(); $scope.$digest();
assert.equal(controller.timestamp, 'a while ago'); assert.equal(controller.timestamp, 'a while ago');
}); });
it('is updated after a timeout', function() { it('is updated after a timeout', function() {
var controller = createDirective().controller; var controller = createDirective(annotation).controller;
fakeTime.nextFuzzyUpdate.returns(10); fakeTime.nextFuzzyUpdate.returns(10);
fakeTime.toFuzzyString.returns('ages ago'); fakeTime.toFuzzyString.returns('ages ago');
$scope.$digest(); $scope.$digest();
...@@ -827,7 +1083,7 @@ describe('annotation.js', function() { ...@@ -827,7 +1083,7 @@ describe('annotation.js', function() {
}); });
it('is no longer updated after the scope is destroyed', function() { it('is no longer updated after the scope is destroyed', function() {
var controller = createDirective().controller; var controller = createDirective(annotation).controller;
$scope.$digest(); $scope.$digest();
$scope.$destroy(); $scope.$destroy();
$timeout.flush(); $timeout.flush();
...@@ -837,13 +1093,10 @@ describe('annotation.js', function() { ...@@ -837,13 +1093,10 @@ describe('annotation.js', function() {
describe('share', function() { describe('share', function() {
it('sets and unsets the open class on the share wrapper', function() { it('sets and unsets the open class on the share wrapper', function() {
var components = createDirective(); var parts = createDirective();
var controller = components.controller; var dialog = parts.element.find('.share-dialog-wrapper');
var element = components.element;
var scope = components.scope;
var dialog = element.find('.share-dialog-wrapper');
dialog.find('button').click(); dialog.find('button').click();
scope.$digest(); parts.scope.$digest();
assert.ok(dialog.hasClass('open')); assert.ok(dialog.hasClass('open'));
documentService().click(); documentService().click();
assert.notOk(dialog.hasClass('open')); assert.notOk(dialog.hasClass('open'));
...@@ -860,11 +1113,13 @@ describe('annotation.js', function() { ...@@ -860,11 +1113,13 @@ describe('annotation.js', function() {
it( it(
'calls annotationMapper.delete() if the delete is confirmed', 'calls annotationMapper.delete() if the delete is confirmed',
function(done) { function(done) {
var controller = createDirective().controller; var parts = createDirective();
sandbox.stub($window, 'confirm').returns(true); sandbox.stub($window, 'confirm').returns(true);
fakeAnnotationMapper.deleteAnnotation.returns($q.resolve()); fakeAnnotationMapper.deleteAnnotation.returns($q.resolve());
controller['delete']().then(function() { parts.controller['delete']().then(function() {
assert(fakeAnnotationMapper.deleteAnnotation.calledWith(annotation)); assert(
fakeAnnotationMapper.deleteAnnotation.calledWith(
parts.annotation));
done(); done();
}); });
$timeout.flush(); $timeout.flush();
...@@ -926,13 +1181,16 @@ describe('annotation.js', function() { ...@@ -926,13 +1181,16 @@ describe('annotation.js', function() {
}); });
describe('saving a new annotation', function() { describe('saving a new annotation', function() {
var annotation;
beforeEach(function() { beforeEach(function() {
fakeFlash.error = sandbox.stub(); fakeFlash.error = sandbox.stub();
annotation = defaultAnnotation();
annotation.$create = sandbox.stub(); annotation.$create = sandbox.stub();
}); });
function controllerWithActionCreate() { function controllerWithActionCreate() {
var controller = createDirective().controller; var controller = createDirective(annotation).controller;
controller.action = 'create'; controller.action = 'create';
return controller; return controller;
} }
...@@ -994,13 +1252,16 @@ describe('annotation.js', function() { ...@@ -994,13 +1252,16 @@ describe('annotation.js', function() {
}); });
describe('saving an edited an annotation', function() { describe('saving an edited an annotation', function() {
var annotation;
beforeEach(function() { beforeEach(function() {
fakeFlash.error = sandbox.stub(); fakeFlash.error = sandbox.stub();
annotation = defaultAnnotation();
annotation.$update = sandbox.stub(); annotation.$update = sandbox.stub();
}); });
function controllerWithActionEdit() { function controllerWithActionEdit() {
var controller = createDirective().controller; var controller = createDirective(annotation).controller;
controller.action = 'edit'; controller.action = 'edit';
return controller; return controller;
} }
...@@ -1050,9 +1311,9 @@ describe('annotation.js', function() { ...@@ -1050,9 +1311,9 @@ describe('annotation.js', function() {
describe('drafts', function() { describe('drafts', function() {
it('creates a draft when editing an annotation', function() { it('creates a draft when editing an annotation', function() {
var controller = createDirective().controller; var parts = createDirective();
controller.edit(); parts.controller.edit();
assert.calledWith(fakeDrafts.update, annotation); assert.calledWith(fakeDrafts.update, parts.annotation);
}); });
it( it(
...@@ -1062,14 +1323,15 @@ describe('annotation.js', function() { ...@@ -1062,14 +1323,15 @@ describe('annotation.js', function() {
// "changes" object that aren't actually set on the annotation. In this // "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 // case, both permissions and tags are null so shouldn't be saved in
// the draft. // the draft.
var controller = createDirective().controller; var parts = createDirective();
annotation.permissions = null; parts.annotation.permissions = null;
annotation.text = 'Hello!'; parts.annotation.text = 'Hello!';
annotation.tags = null; parts.annotation.tags = null;
controller.edit(); parts.controller.edit();
assert.calledWith(fakeDrafts.update, annotation, {text: 'Hello!'}); assert.calledWith(
fakeDrafts.update, parts.annotation, {text: 'Hello!'});
} }
); );
...@@ -1101,15 +1363,16 @@ describe('annotation.js', function() { ...@@ -1101,15 +1363,16 @@ describe('annotation.js', function() {
}); });
it('removes the draft when changes are discarded', function() { it('removes the draft when changes are discarded', function() {
var controller = createDirective().controller; var parts = createDirective();
controller.edit(); parts.controller.edit();
controller.revert(); parts.controller.revert();
assert.calledWith(fakeDrafts.remove, annotation); assert.calledWith(fakeDrafts.remove, parts.annotation);
}); });
it('removes the draft when changes are saved', function() { it('removes the draft when changes are saved', function() {
var annotation = defaultAnnotation();
annotation.$update = sandbox.stub().returns(Promise.resolve()); annotation.$update = sandbox.stub().returns(Promise.resolve());
var controller = createDirective().controller; var controller = createDirective(annotation).controller;
controller.edit(); controller.edit();
controller.save(); controller.save();
...@@ -1125,17 +1388,17 @@ describe('annotation.js', function() { ...@@ -1125,17 +1388,17 @@ describe('annotation.js', function() {
describe('when the focused group changes', function() { describe('when the focused group changes', function() {
it('updates the current draft', function() { it('updates the current draft', function() {
var controller = createDirective().controller; var parts = createDirective();
controller.edit(); parts.controller.edit();
controller.annotation.text = 'unsaved-text'; parts.controller.annotation.text = 'unsaved-text';
controller.annotation.tags = []; parts.controller.annotation.tags = [];
controller.annotation.permissions = 'new permissions'; parts.controller.annotation.permissions = 'new permissions';
fakeDrafts.get = sinon.stub().returns({ fakeDrafts.get = sinon.stub().returns({
text: 'old-draft' text: 'old-draft'
}); });
fakeDrafts.update = sinon.stub(); fakeDrafts.update = sinon.stub();
$rootScope.$broadcast(events.GROUP_FOCUSED); $rootScope.$broadcast(events.GROUP_FOCUSED);
assert.calledWith(fakeDrafts.update, annotation, { assert.calledWith(fakeDrafts.update, parts.annotation, {
text: 'unsaved-text', text: 'unsaved-text',
tags: [], tags: [],
permissions: 'new permissions' permissions: 'new permissions'
...@@ -1152,8 +1415,9 @@ describe('annotation.js', function() { ...@@ -1152,8 +1415,9 @@ describe('annotation.js', function() {
}); });
it('moves new annotations to the focused group', function() { it('moves new annotations to the focused group', function() {
var annotation = defaultAnnotation();
annotation.id = null; annotation.id = null;
createDirective(); createDirective(annotation);
fakeGroups.focused = sinon.stub().returns({ fakeGroups.focused = sinon.stub().returns({
id: 'new-group' id: 'new-group'
}); });
...@@ -1167,6 +1431,7 @@ describe('annotation.js', function() { ...@@ -1167,6 +1431,7 @@ describe('annotation.js', function() {
function() { function() {
// id must be null so that AnnotationController considers this a new // id must be null so that AnnotationController considers this a new
// annotation. // annotation.
var annotation = defaultAnnotation();
annotation.id = null; annotation.id = null;
annotation.group = 'old-group'; annotation.group = 'old-group';
annotation.permissions = { annotation.permissions = {
...@@ -1174,7 +1439,7 @@ describe('annotation.js', function() { ...@@ -1174,7 +1439,7 @@ describe('annotation.js', function() {
}; };
// This is a shared annotation. // This is a shared annotation.
fakePermissions.isShared.returns(true); fakePermissions.isShared.returns(true);
createDirective(); createDirective(annotation);
// Make permissions.shared() behave like we expect it to. // Make permissions.shared() behave like we expect it to.
fakePermissions.shared = function(groupId) { fakePermissions.shared = function(groupId) {
return { return {
...@@ -1192,6 +1457,7 @@ describe('annotation.js', function() { ...@@ -1192,6 +1457,7 @@ describe('annotation.js', function() {
it('saves shared permissions for the new group to drafts', function() { it('saves shared permissions for the new group to drafts', function() {
// id must be null so that AnnotationController considers this a new // id must be null so that AnnotationController considers this a new
// annotation. // annotation.
var annotation = defaultAnnotation();
annotation.id = null; annotation.id = null;
annotation.group = 'old-group'; annotation.group = 'old-group';
annotation.permissions = { annotation.permissions = {
...@@ -1199,7 +1465,7 @@ describe('annotation.js', function() { ...@@ -1199,7 +1465,7 @@ describe('annotation.js', function() {
}; };
// This is a shared annotation. // This is a shared annotation.
fakePermissions.isShared.returns(true); fakePermissions.isShared.returns(true);
createDirective(); createDirective(annotation);
// drafts.get() needs to return something truthy, otherwise // drafts.get() needs to return something truthy, otherwise
// AnnotationController won't try to update the draft for the annotation. // AnnotationController won't try to update the draft for the annotation.
fakeDrafts.get.returns(true); fakeDrafts.get.returns(true);
...@@ -1223,12 +1489,13 @@ describe('annotation.js', function() { ...@@ -1223,12 +1489,13 @@ describe('annotation.js', function() {
it('does not change perms when moving new private annotations', function() { it('does not change perms when moving new private annotations', function() {
// id must be null so that AnnotationController considers this a new // id must be null so that AnnotationController considers this a new
// annotation. // annotation.
var annotation = defaultAnnotation();
annotation.id = null; annotation.id = null;
annotation.group = 'old-group'; annotation.group = 'old-group';
annotation.permissions = { annotation.permissions = {
read: ['acct:bill@localhost'] read: ['acct:bill@localhost']
}; };
createDirective(); createDirective(annotation);
// This is a private annotation. // This is a private annotation.
fakePermissions.isShared.returns(false); fakePermissions.isShared.returns(false);
fakeGroups.focused = sinon.stub().returns({ fakeGroups.focused = sinon.stub().returns({
......
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