Add collection to app.views.NotificationDropdown and app.views.Notifications

closes #6952
This commit is contained in:
Augier 2016-08-08 16:56:02 +02:00 committed by Steffen van Bergerem
parent 9b72527f3e
commit af331bfb30
No known key found for this signature in database
GPG key ID: 315C9787D548DC6B
20 changed files with 875 additions and 304 deletions

View file

@ -19,6 +19,8 @@
* Add a dark color theme [#7152](https://github.com/diaspora/diaspora/pull/7152) * Add a dark color theme [#7152](https://github.com/diaspora/diaspora/pull/7152)
* Added setting for custom changelog URL [#7166](https://github.com/diaspora/diaspora/pull/7166) * Added setting for custom changelog URL [#7166](https://github.com/diaspora/diaspora/pull/7166)
* Show more information of recipients on conversation creation [#7129](https://github.com/diaspora/diaspora/pull/7129) * Show more information of recipients on conversation creation [#7129](https://github.com/diaspora/diaspora/pull/7129)
* Update notifications every 5 minutes and when opening the notification dropdown [#6952](https://github.com/diaspora/diaspora/pull/6952)
* Show browser notifications when receiving new unread notifications [#6952](https://github.com/diaspora/diaspora/pull/6952)
# 0.6.1.0 # 0.6.1.0

View file

@ -90,6 +90,7 @@ var app = {
setupHeader: function() { setupHeader: function() {
if(app.currentUser.authenticated()) { if(app.currentUser.authenticated()) {
app.notificationsCollection = new app.collections.Notifications();
app.header = new app.views.Header(); app.header = new app.views.Header();
$("header").prepend(app.header.el); $("header").prepend(app.header.el);
app.header.render(); app.header.render();

View file

@ -0,0 +1,118 @@
app.collections.Notifications = Backbone.Collection.extend({
model: app.models.Notification,
// URL parameter
/* eslint-disable camelcase */
url: Routes.notifications({per_page: 10, page: 1}),
/* eslint-enable camelcase */
page: 2,
perPage: 5,
unreadCount: 0,
unreadCountByType: {},
timeout: 300000, // 5 minutes
initialize: function() {
this.pollNotifications();
setTimeout(function() {
setInterval(this.pollNotifications.bind(this), this.timeout);
}.bind(this), this.timeout);
Diaspora.BrowserNotification.requestPermission();
},
pollNotifications: function() {
var unreadCountBefore = this.unreadCount;
this.fetch();
this.once("finishedLoading", function() {
if (unreadCountBefore < this.unreadCount) {
Diaspora.BrowserNotification.spawnNotification(
Diaspora.I18n.t("notifications.new_notifications", {count: this.unreadCount}));
}
}, this);
},
fetch: function(options) {
options = options || {};
options.remove = false;
options.merge = true;
options.parse = true;
Backbone.Collection.prototype.fetch.apply(this, [options]);
},
fetchMore: function() {
var hasMoreNotifications = (this.page * this.perPage) <= this.length;
// There are more notifications to load on the current page
if (hasMoreNotifications) {
this.page++;
// URL parameter
/* eslint-disable camelcase */
var route = Routes.notifications({per_page: this.perPage, page: this.page});
/* eslint-enable camelcase */
this.fetch({url: route, pushBack: true});
}
},
/**
* Adds new models to the collection at the end or at the beginning of the collection and
* then fires an event for each model of the collection. It will fire a different event
* based on whether the models were added at the end (typically when the scroll triggers to load more
* notifications) or at the beginning (new notifications have been added to the front of the list).
*/
set: function(items, options) {
options = options || {};
options.at = options.pushBack ? this.length : 0;
// Retreive back the new created models
var models = [];
var accu = function(model) { models.push(model); };
this.on("add", accu);
Backbone.Collection.prototype.set.apply(this, [items, options]);
this.off("add", accu);
if (options.pushBack) {
models.forEach(function(model) { this.trigger("pushBack", model); }.bind(this));
} else {
// Fires events in the reverse order so that the first event is prepended in first position
models.reverse();
models.forEach(function(model) { this.trigger("pushFront", model); }.bind(this));
}
this.trigger("finishedLoading");
},
parse: function(response) {
this.unreadCount = response.unread_count;
this.unreadCountByType = response.unread_count_by_type;
return _.map(response.notification_list, function(item) {
/* eslint-disable new-cap */
var model = new this.model(item);
/* eslint-enable new-cap */
model.on("change:unread", this.onChangedUnreadStatus.bind(this));
return model;
}.bind(this));
},
setAllRead: function() {
this.forEach(function(model) { model.setRead(); });
},
setRead: function(guid) {
this.find(function(model) { return model.guid === guid; }).setRead();
},
setUnread: function(guid) {
this.find(function(model) { return model.guid === guid; }).setUnread();
},
onChangedUnreadStatus: function(model) {
if (model.get("unread") === true) {
this.unreadCount++;
this.unreadCountByType[model.get("type")]++;
} else {
this.unreadCount = Math.max(this.unreadCount - 1, 0);
this.unreadCountByType[model.get("type")] = Math.max(this.unreadCountByType[model.get("type")] - 1, 0);
}
this.trigger("update");
}
});

View file

@ -0,0 +1,69 @@
app.models.Notification = Backbone.Model.extend({
constructor: function(attributes, options) {
options = options || {};
options.parse = true;
Backbone.Model.apply(this, [attributes, options]);
this.guid = this.get("id");
},
/**
* Flattens the notification object returned by the server.
*
* The server returns an object that looks like:
*
* {
* "reshared": {
* "id": 45,
* "target_type": "Post",
* "target_id": 11,
* "recipient_id": 1,
* "unread": true,
* "created_at": "2015-10-27T19:56:30.000Z",
* "updated_at": "2015-10-27T19:56:30.000Z",
* "note_html": <html/>
* },
* "type": "reshared"
* }
*
* The returned object looks like:
*
* {
* "type": "reshared",
* "id": 45,
* "target_type": "Post",
* "target_id": 11,
* "recipient_id": 1,
* "unread": true,
* "created_at": "2015-10-27T19:56:30.000Z",
* "updated_at": "2015-10-27T19:56:30.000Z",
* "note_html": <html/>,
* }
*/
parse: function(response) {
var result = {type: response.type};
result = $.extend(result, response[result.type]);
return result;
},
setRead: function() {
this.setUnreadStatus(false);
},
setUnread: function() {
this.setUnreadStatus(true);
},
setUnreadStatus: function(state) {
if (this.get("unread") !== state) {
$.ajax({
url: Routes.notification(this.guid),
/* eslint-disable camelcase */
data: {set_unread: state},
/* eslint-enable camelcase */
type: "PUT",
context: this,
success: function() { this.set("unread", state); }
});
}
}
});

View file

@ -139,7 +139,7 @@ app.Router = Backbone.Router.extend({
notifications: function() { notifications: function() {
this._loadContacts(); this._loadContacts();
this.renderAspectMembershipDropdowns($(document)); this.renderAspectMembershipDropdowns($(document));
new app.views.Notifications({el: "#notifications_container"}); new app.views.Notifications({el: "#notifications_container", collection: app.notificationsCollection});
}, },
peopleSearch: function() { peopleSearch: function() {

View file

@ -12,12 +12,12 @@ app.views.Header = app.views.Base.extend({
}); });
}, },
postRenderTemplate: function(){ postRenderTemplate: function() {
new app.views.Notifications({ el: "#notification-dropdown" }); new app.views.Notifications({el: "#notification-dropdown", collection: app.notificationsCollection});
this.notificationDropdown = new app.views.NotificationDropdown({ el: "#notification-dropdown" }); new app.views.NotificationDropdown({el: "#notification-dropdown", collection: app.notificationsCollection});
new app.views.Search({ el: "#header-search-form" }); new app.views.Search({el: "#header-search-form"});
}, },
menuElement: function(){ return this.$("ul.dropdown"); }, menuElement: function() { return this.$("ul.dropdown"); }
}); });
// @license-end // @license-end

View file

@ -6,16 +6,21 @@ app.views.NotificationDropdown = app.views.Base.extend({
}, },
initialize: function(){ initialize: function(){
$(document.body).click($.proxy(this.hideDropdown, this)); $(document.body).click(this.hideDropdown.bind(this));
this.notifications = [];
this.perPage = 5;
this.hasMoreNotifs = true;
this.badge = this.$el; this.badge = this.$el;
this.dropdown = $("#notification-dropdown"); this.dropdown = $("#notification-dropdown");
this.dropdownNotifications = this.dropdown.find(".notifications"); this.dropdownNotifications = this.dropdown.find(".notifications");
this.ajaxLoader = this.dropdown.find(".ajax-loader"); this.ajaxLoader = this.dropdown.find(".ajax-loader");
this.perfectScrollbarInitialized = false; this.perfectScrollbarInitialized = false;
this.dropdownNotifications.scroll(this.dropdownScroll.bind(this));
this.bindCollectionEvents();
},
bindCollectionEvents: function() {
this.collection.on("pushFront", this.onPushFront.bind(this));
this.collection.on("pushBack", this.onPushBack.bind(this));
this.collection.on("finishedLoading", this.finishLoading.bind(this));
}, },
toggleDropdown: function(evt){ toggleDropdown: function(evt){
@ -31,12 +36,11 @@ app.views.NotificationDropdown = app.views.Base.extend({
}, },
showDropdown: function(){ showDropdown: function(){
this.resetParams();
this.ajaxLoader.show(); this.ajaxLoader.show();
this.dropdown.addClass("dropdown-open"); this.dropdown.addClass("dropdown-open");
this.updateScrollbar(); this.updateScrollbar();
this.dropdownNotifications.addClass("loading"); this.dropdownNotifications.addClass("loading");
this.getNotifications(); this.collection.fetch();
}, },
hideDropdown: function(evt){ hideDropdown: function(evt){
@ -50,40 +54,18 @@ app.views.NotificationDropdown = app.views.Base.extend({
dropdownScroll: function(){ dropdownScroll: function(){
var isLoading = ($(".loading").length === 1); var isLoading = ($(".loading").length === 1);
if (this.isBottom() && this.hasMoreNotifs && !isLoading){ if (this.isBottom() && !isLoading) {
this.dropdownNotifications.addClass("loading"); this.dropdownNotifications.addClass("loading");
this.getNotifications(); this.collection.fetchMore();
} }
}, },
getParams: function(){
if(this.notifications.length === 0){ return{ per_page: 10, page: 1 }; }
else{ return{ per_page: this.perPage, page: this.nextPage }; }
},
resetParams: function(){
this.notifications.length = 0;
this.hasMoreNotifs = true;
delete this.nextPage;
},
isBottom: function(){ isBottom: function(){
var bottom = this.dropdownNotifications.prop("scrollHeight") - this.dropdownNotifications.height(); var bottom = this.dropdownNotifications.prop("scrollHeight") - this.dropdownNotifications.height();
var currentPosition = this.dropdownNotifications.scrollTop(); var currentPosition = this.dropdownNotifications.scrollTop();
return currentPosition + 50 >= bottom; return currentPosition + 50 >= bottom;
}, },
getNotifications: function(){
var self = this;
$.getJSON(Routes.notifications(this.getParams()), function(notifications){
$.each(notifications, function(){ self.notifications.push(this); });
self.hasMoreNotifs = notifications.length >= self.perPage;
if(self.nextPage){ self.nextPage++; }
else { self.nextPage = 3; }
self.renderNotifications();
});
},
hideAjaxLoader: function(){ hideAjaxLoader: function(){
var self = this; var self = this;
this.ajaxLoader.find(".spinner").fadeTo(200, 0, function(){ this.ajaxLoader.find(".spinner").fadeTo(200, 0, function(){
@ -93,28 +75,23 @@ app.views.NotificationDropdown = app.views.Base.extend({
}); });
}, },
renderNotifications: function(){ onPushBack: function(notification) {
var self = this; var node = this.dropdownNotifications.append(notification.get("note_html"));
this.dropdownNotifications.find(".media.stream-element").remove(); $(node).find(".unread-toggle .entypo-eye").tooltip("destroy").tooltip();
$.each(self.notifications, function(index, notifications){ $(node).find(this.avatars.selector).error(this.avatars.fallback);
$.each(notifications, function(index, notification){ },
if($.inArray(notification, notifications) === -1){
var node = self.dropdownNotifications.append(notification.note_html);
$(node).find(".unread-toggle .entypo-eye").tooltip("destroy").tooltip();
$(node).find(self.avatars.selector).error(self.avatars.fallback);
}
});
});
this.hideAjaxLoader(); onPushFront: function(notification) {
var node = this.dropdownNotifications.prepend(notification.get("note_html"));
$(node).find(".unread-toggle .entypo-eye").tooltip("destroy").tooltip();
$(node).find(this.avatars.selector).error(this.avatars.fallback);
},
finishLoading: function() {
app.helpers.timeago(this.dropdownNotifications); app.helpers.timeago(this.dropdownNotifications);
this.updateScrollbar(); this.updateScrollbar();
this.hideAjaxLoader();
this.dropdownNotifications.removeClass("loading"); this.dropdownNotifications.removeClass("loading");
this.dropdownNotifications.scroll(function(){
self.dropdownScroll();
});
}, },
updateScrollbar: function() { updateScrollbar: function() {

View file

@ -1,96 +1,85 @@
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
app.views.Notifications = Backbone.View.extend({ app.views.Notifications = Backbone.View.extend({
events: { events: {
"click .unread-toggle" : "toggleUnread", "click .unread-toggle": "toggleUnread",
"click #mark_all_read_link": "markAllRead" "click #mark-all-read-link": "markAllRead"
}, },
initialize: function() { initialize: function() {
$(".unread-toggle .entypo-eye").tooltip(); $(".unread-toggle .entypo-eye").tooltip();
app.helpers.timeago($(document)); app.helpers.timeago($(document));
this.bindCollectionEvents();
},
bindCollectionEvents: function() {
this.collection.on("change", this.onChangedUnreadStatus.bind(this));
this.collection.on("update", this.updateView.bind(this));
}, },
toggleUnread: function(evt) { toggleUnread: function(evt) {
var note = $(evt.target).closest(".stream-element"); var note = $(evt.target).closest(".stream-element");
var unread = note.hasClass("unread"); var unread = note.hasClass("unread");
var guid = note.data("guid"); var guid = note.data("guid");
if (unread){ this.setRead(guid); } if (unread) {
else { this.setUnread(guid); } this.collection.setRead(guid);
}, } else {
this.collection.setUnread(guid);
getAllUnread: function() { return $(".media.stream-element.unread"); },
setRead: function(guid) { this.setUnreadStatus(guid, false); },
setUnread: function(guid){ this.setUnreadStatus(guid, true); },
setUnreadStatus: function(guid, state){
$.ajax({
url: "/notifications/" + guid,
data: { set_unread: state },
type: "PUT",
context: this,
success: this.clickSuccess
});
},
clickSuccess: function(data) {
var guid = data.guid;
var type = $(".stream-element[data-guid=" + guid + "]").data("type");
this.updateView(guid, type, data.unread);
},
markAllRead: function(evt){
if(evt) { evt.preventDefault(); }
var self = this;
this.getAllUnread().each(function(i, el){
self.setRead($(el).data("guid"));
});
},
updateView: function(guid, type, unread) {
var change = unread ? 1 : -1,
allNotes = $("#notifications_container .list-group > a:eq(0) .badge"),
typeNotes = $("#notifications_container .list-group > a[data-type=" + type + "] .badge"),
headerBadge = $(".notifications-link .badge"),
note = $(".notifications .stream-element[data-guid=" + guid + "]"),
markAllReadLink = $("a#mark_all_read_link"),
translationKey = unread ? "notifications.mark_read" : "notifications.mark_unread";
if(unread){ note.removeClass("read").addClass("unread"); }
else { note.removeClass("unread").addClass("read"); }
$(".unread-toggle .entypo-eye", note)
.tooltip("destroy")
.removeAttr("data-original-title")
.attr("title",Diaspora.I18n.t(translationKey))
.tooltip();
[allNotes, typeNotes, headerBadge].forEach(function(element){
element.text(function(i, text){
return parseInt(text) + change;
});
});
[allNotes, typeNotes].forEach(function(badge) {
if(badge.text() > 0) {
badge.removeClass("hidden");
}
else {
badge.addClass("hidden");
}
});
if(headerBadge.text() > 0){
headerBadge.removeClass("hidden");
markAllReadLink.removeClass("disabled");
} }
else{ },
headerBadge.addClass("hidden");
markAllRead: function() {
this.collection.setAllRead();
},
onChangedUnreadStatus: function(model) {
var unread = model.get("unread");
var translationKey = unread ? "notifications.mark_read" : "notifications.mark_unread";
var note = $(".stream-element[data-guid=" + model.guid + "]");
note.find(".entypo-eye")
.tooltip("destroy")
.removeAttr("data-original-title")
.attr("title", Diaspora.I18n.t(translationKey))
.tooltip();
if (unread) {
note.removeClass("read").addClass("unread");
} else {
note.removeClass("unread").addClass("read");
}
},
updateView: function() {
var notificationsContainer = $("#notifications_container");
// update notification counts in the sidebar
Object.keys(this.collection.unreadCountByType).forEach(function(notificationType) {
var count = this.collection.unreadCountByType[notificationType];
this.updateBadge(notificationsContainer.find("a[data-type=" + notificationType + "] .badge"), count);
}.bind(this));
this.updateBadge(notificationsContainer.find("a[data-type=all] .badge"), this.collection.unreadCount);
// update notification count in the header
this.updateBadge($(".notifications-link .badge"), this.collection.unreadCount);
var markAllReadLink = $("a#mark-all-read-link");
if (this.collection.unreadCount > 0) {
markAllReadLink.removeClass("disabled");
} else {
markAllReadLink.addClass("disabled"); markAllReadLink.addClass("disabled");
} }
},
updateBadge: function(badge, count) {
badge.text(count);
if (count > 0) {
badge.removeClass("hidden");
} else {
badge.addClass("hidden");
}
} }
}); });
// @license-end // @license-end

View file

@ -0,0 +1,22 @@
Diaspora.BrowserNotification = {
requestPermission: function() {
if ("Notification" in window && Notification.permission !== "granted" && Notification.permission !== "denied") {
Notification.requestPermission();
}
},
spawnNotification: function(title, summary) {
if ("Notification" in window && Notification.permission === "granted") {
if (!_.isString(title)) {
throw new Error("No notification title given.");
}
summary = summary || "";
new Notification(title, {
body: summary,
icon: ImagePaths.get("branding/logos/asterisk_white_mobile.png")
});
}
}
};

View file

@ -53,7 +53,7 @@
<ul class="dropdown-menu" role="menu"> <ul class="dropdown-menu" role="menu">
<div class="header"> <div class="header">
<div class="pull-right"> <div class="pull-right">
<a href="#" id="mark_all_read_link" class="btn btn-default btn-sm {{#unless current_user.notifications_count}}disabled{{/unless}}"> <a href="#" id="mark-all-read-link" class="btn btn-default btn-sm {{#unless current_user.notifications_count}}disabled{{/unless}}">
{{t "header.mark_all_as_read"}} {{t "header.mark_all_as_read"}}
</a> </a>
</div> </div>
@ -61,11 +61,10 @@
{{t "header.recent_notifications"}} {{t "header.recent_notifications"}}
</h4> </h4>
</div> </div>
<div class="notifications"> <div class="ajax-loader">
<div class="ajax-loader"> <div class="spinner"></div>
<div class="spinner"></div>
</div>
</div> </div>
<div class="notifications"></div>
<div class="view_all"> <div class="view_all">
<a href="/notifications" id="view_all_notifications"> <a href="/notifications" id="view_all_notifications">
{{t "header.view_all"}} {{t "header.view_all"}}

View file

@ -52,7 +52,7 @@ class NotificationsController < ApplicationController
format.html format.html
format.xml { render :xml => @notifications.to_xml } format.xml { render :xml => @notifications.to_xml }
format.json { format.json {
render json: @notifications, each_serializer: NotificationSerializer render json: render_as_json(@unread_notification_count, @grouped_unread_notification_counts, @notifications)
} }
end end
end end
@ -82,4 +82,15 @@ class NotificationsController < ApplicationController
end end
end end
private
def render_as_json(unread_count, unread_count_by_type, notification_list)
{
unread_count: unread_count,
unread_count_by_type: unread_count_by_type,
notification_list: notification_list.map {|note|
NotificationSerializer.new(note, default_serializer_options).as_json
}
}.as_json
end
end end

View file

@ -7,7 +7,8 @@
= t(".notifications") = t(".notifications")
.list-group .list-group
%a.list-group-item{href: "/notifications" + (params[:show] == "unread" ? "?show=unread" : ""), %a.list-group-item{href: "/notifications" + (params[:show] == "unread" ? "?show=unread" : ""),
class: ("active" unless params[:type] && @grouped_unread_notification_counts.has_key?(params[:type]))} class: ("active" unless params[:type] && @grouped_unread_notification_counts.has_key?(params[:type])),
data: {type: "all"}}
%span.pull-right.badge{class: ("hidden" unless @unread_notification_count > 0)} %span.pull-right.badge{class: ("hidden" unless @unread_notification_count > 0)}
= @unread_notification_count = @unread_notification_count
= t(".all_notifications") = t(".all_notifications")

View file

@ -248,6 +248,9 @@ en:
notifications: notifications:
mark_read: "Mark read" mark_read: "Mark read"
mark_unread: "Mark unread" mark_unread: "Mark unread"
new_notifications:
one: "You have <%= count %> unread notification"
other: "You have <%= count %> unread notifications"
stream: stream:
hide: "Hide" hide: "Hide"

View file

@ -14,6 +14,8 @@ describe NotificationsController, :type => :controller do
it "generates a jasmine fixture", :fixture => true do it "generates a jasmine fixture", :fixture => true do
get :index get :index
save_fixture(html_for("body"), "notifications") save_fixture(html_for("body"), "notifications")
get :index, format: :json
save_fixture(response.body, "notifications_collection")
end end
end end
end end

View file

@ -68,15 +68,24 @@ describe NotificationsController, :type => :controller do
expect(assigns[:notifications].count).to eq(1) expect(assigns[:notifications].count).to eq(1)
end end
it 'succeeds for notification dropdown' do it "succeeds for notification dropdown" do
Timecop.travel(6.seconds.ago) do Timecop.travel(6.seconds.ago) do
@notification.touch @notification.touch
end end
get :index, :format => :json get :index, format: :json
expect(response).to be_success expect(response).to be_success
note_html = JSON.parse(response.body)[0]["also_commented"]["note_html"] response_json = JSON.parse(response.body)
note_html = Nokogiri::HTML(note_html) note_html = Nokogiri::HTML(response_json["notification_list"][0]["also_commented"]["note_html"])
timeago_content = note_html.css("time")[0]["data-time-ago"] timeago_content = note_html.css("time")[0]["data-time-ago"]
expect(response_json["unread_count"]).to be(1)
expect(response_json["unread_count_by_type"]).to eq(
"also_commented" => 1,
"comment_on_post" => 0,
"liked" => 0,
"mentioned" => 0,
"reshared" => 0,
"started_sharing" => 0
)
expect(timeago_content).to include(@notification.updated_at.iso8601) expect(timeago_content).to include(@notification.updated_at.iso8601)
expect(response.body).to match(/note_html/) expect(response.body).to match(/note_html/)
end end

View file

@ -0,0 +1,249 @@
describe("app.collections.Notifications", function() {
describe("initialize", function() {
it("calls pollNotifications", function() {
spyOn(app.collections.Notifications.prototype, "pollNotifications");
new app.collections.Notifications();
expect(app.collections.Notifications.prototype.pollNotifications).toHaveBeenCalled();
});
it("calls Diaspora.BrowserNotification.requestPermission", function() {
spyOn(Diaspora.BrowserNotification, "requestPermission");
new app.collections.Notifications();
expect(Diaspora.BrowserNotification.requestPermission).toHaveBeenCalled();
});
it("initializes attributes", function() {
var target = new app.collections.Notifications();
expect(target.model).toBe(app.models.Notification);
/* eslint-disable camelcase */
expect(target.url).toBe(Routes.notifications({per_page: 10, page: 1}));
/* eslint-enable camelcase */
expect(target.page).toBe(2);
expect(target.perPage).toBe(5);
expect(target.unreadCount).toBe(0);
expect(target.unreadCountByType).toEqual({});
});
});
describe("pollNotifications", function() {
beforeEach(function() {
this.target = new app.collections.Notifications();
});
it("calls fetch", function() {
spyOn(app.collections.Notifications.prototype, "fetch");
this.target.pollNotifications();
expect(app.collections.Notifications.prototype.fetch).toHaveBeenCalled();
});
it("doesn't call Diaspora.BrowserNotification.spawnNotification when there are no new notifications", function() {
spyOn(Diaspora.BrowserNotification, "spawnNotification");
this.target.pollNotifications();
this.target.trigger("finishedLoading");
expect(Diaspora.BrowserNotification.spawnNotification).not.toHaveBeenCalled();
});
it("calls Diaspora.BrowserNotification.spawnNotification when there are new notifications", function() {
spyOn(Diaspora.BrowserNotification, "spawnNotification");
spyOn(app.collections.Notifications.prototype, "fetch").and.callFake(function() {
this.target.unreadCount++;
}.bind(this));
this.target.pollNotifications();
this.target.trigger("finishedLoading");
expect(Diaspora.BrowserNotification.spawnNotification).toHaveBeenCalled();
});
it("refreshes after timeout", function() {
spyOn(app.collections.Notifications.prototype, "pollNotifications").and.callThrough();
this.target.pollNotifications();
expect(app.collections.Notifications.prototype.pollNotifications).toHaveBeenCalledTimes(1);
jasmine.clock().tick(2 * this.target.timeout);
expect(app.collections.Notifications.prototype.pollNotifications).toHaveBeenCalledTimes(2);
});
});
describe("fetch", function() {
it("calls Backbone.Collection.prototype.fetch with correct parameters", function() {
var target = new app.collections.Notifications();
spyOn(Backbone.Collection.prototype, "fetch");
target.fetch({foo: "bar", remove: "bar", merge: "bar", parse: "bar"});
expect(Backbone.Collection.prototype.fetch.calls.mostRecent().args).toEqual([{
foo: "bar",
remove: false,
merge: true,
parse: true
}]);
});
});
describe("fetchMore", function() {
beforeEach(function() {
this.target = new app.collections.Notifications();
spyOn(app.collections.Notifications.prototype, "fetch");
});
it("fetches notifications when there are more notifications to be fetched", function() {
this.target.length = 15;
this.target.fetchMore();
/* eslint-disable camelcase */
var route = Routes.notifications({per_page: 5, page: 3});
/* eslint-enable camelcase */
expect(app.collections.Notifications.prototype.fetch).toHaveBeenCalledWith({url: route, pushBack: true});
expect(this.target.page).toBe(3);
});
it("doesn't fetch notifications when there are no more notifications to be fetched", function() {
this.target.length = 0;
this.target.fetchMore();
expect(app.collections.Notifications.prototype.fetch).not.toHaveBeenCalled();
expect(this.target.page).toBe(2);
});
});
describe("set", function() {
beforeEach(function() {
this.target = new app.collections.Notifications();
});
context("calls to Backbone.Collection.prototype.set", function() {
beforeEach(function() {
spyOn(Backbone.Collection.prototype, "set");
});
it("calls app.collections.Notifications.prototype.set", function() {
this.target.set([]);
expect(Backbone.Collection.prototype.set).toHaveBeenCalledWith([], {at: 0});
});
it("inserts the items at the beginning of the collection if option 'pushBack' is false", function() {
this.target.length = 15;
this.target.set([], {pushBack: false});
expect(Backbone.Collection.prototype.set).toHaveBeenCalledWith([], {pushBack: false, at: 0});
});
it("inserts the items at the end of the collection if option 'pushBack' is true", function() {
this.target.length = 15;
this.target.set([], {pushBack: true});
expect(Backbone.Collection.prototype.set).toHaveBeenCalledWith([], {pushBack: true, at: 15});
});
});
context("events", function() {
beforeEach(function() {
spyOn(Backbone.Collection.prototype, "set").and.callThrough();
spyOn(app.collections.Notifications.prototype, "trigger").and.callThrough();
this.model1 = new app.models.Notification({"reshared": {id: 1}, "type": "reshared"});
this.model2 = new app.models.Notification({"reshared": {id: 2}, "type": "reshared"});
this.model3 = new app.models.Notification({"reshared": {id: 3}, "type": "reshared"});
this.model4 = new app.models.Notification({"reshared": {id: 4}, "type": "reshared"});
});
it("triggers a 'pushFront' event for each model in reverse order when option 'pushBack' is false", function() {
this.target.set([this.model1, this.model2, this.model3, this.model4], {pushBack: false});
var calls = app.collections.Notifications.prototype.trigger.calls;
var index = calls.count() - 5;
expect(calls.argsFor(index)).toEqual(["pushFront", this.model4]);
expect(calls.argsFor(index + 1)).toEqual(["pushFront", this.model3]);
expect(calls.argsFor(index + 2)).toEqual(["pushFront", this.model2]);
expect(calls.argsFor(index + 3)).toEqual(["pushFront", this.model1]);
});
it("triggers a 'pushBack' event for each model in normal order when option 'pushBack' is true", function() {
this.target.set([this.model1, this.model2, this.model3, this.model4], {pushBack: true});
var calls = app.collections.Notifications.prototype.trigger.calls;
var index = calls.count() - 5;
expect(calls.argsFor(index)).toEqual(["pushBack", this.model1]);
expect(calls.argsFor(index + 1)).toEqual(["pushBack", this.model2]);
expect(calls.argsFor(index + 2)).toEqual(["pushBack", this.model3]);
expect(calls.argsFor(index + 3)).toEqual(["pushBack", this.model4]);
});
it("triggers a 'finishedLoading' event at the end of the process", function() {
this.target.set([]);
expect(app.collections.Notifications.prototype.trigger).toHaveBeenCalledWith("finishedLoading");
});
});
});
describe("parse", function() {
beforeEach(function() {
this.target = new app.collections.Notifications();
});
it("sets the unreadCount and unreadCountByType attributes", function() {
expect(this.target.unreadCount).toBe(0);
expect(this.target.unreadCountByType).toEqual({});
/* eslint-disable camelcase */
this.target.parse({
unread_count: 15,
unread_count_by_type: {reshared: 6},
notification_list: []
});
/* eslint-enable camelcase */
expect(this.target.unreadCount).toBe(15);
expect(this.target.unreadCountByType).toEqual({reshared: 6});
});
it("correctly parses the result", function() {
/* eslint-disable camelcase */
var parsed = this.target.parse({
unread_count: 15,
unread_count_by_type: {reshared: 6},
notification_list: [{"reshared": {id: 1}, "type": "reshared"}]
});
/* eslint-enable camelcase */
expect(parsed.length).toEqual(1);
});
it("correctly binds the change:unread event", function() {
spyOn(app.collections.Notifications.prototype, "onChangedUnreadStatus");
/* eslint-disable camelcase */
var parsed = this.target.parse({
unread_count: 15,
unread_count_by_type: {reshared: 6},
notification_list: [{"reshared": {id: 1}, "type": "reshared"}]
});
/* eslint-enable camelcase */
parsed[0].set("unread", true);
expect(app.collections.Notifications.prototype.onChangedUnreadStatus).toHaveBeenCalled();
});
});
describe("onChangedUnreadStatus", function() {
it("increases the unread counts when model's unread attribute is true", function() {
var target = new app.collections.Notifications();
var model = new app.models.Notification({"reshared": {id: 1, unread: true}, "type": "reshared"});
target.unreadCount = 15;
target.unreadCountByType.reshared = 6;
target.onChangedUnreadStatus(model);
expect(target.unreadCount).toBe(16);
expect(target.unreadCountByType.reshared).toBe(7);
});
it("decreases the unread counts when model's unread attribute is false", function() {
var target = new app.collections.Notifications();
var model = new app.models.Notification({"reshared": {id: 1, unread: false}, "type": "reshared"});
target.unreadCount = 15;
target.unreadCountByType.reshared = 6;
target.onChangedUnreadStatus(model);
expect(target.unreadCount).toBe(14);
expect(target.unreadCountByType.reshared).toBe(5);
});
});
});

View file

@ -0,0 +1,85 @@
describe("app.models.Notification", function() {
beforeEach(function() {
this.model = new app.models.Notification({
"reshared": {},
"type": "reshared"
});
});
describe("constructor", function() {
it("calls parent constructor with the correct parameters", function() {
spyOn(Backbone, "Model").and.callThrough();
new app.models.Notification({attribute: "attribute"}, {option: "option"});
expect(Backbone.Model).toHaveBeenCalledWith(
{attribute: "attribute"},
{option: "option", parse: true}
);
});
});
describe("parse", function() {
it("correctly parses the object", function() {
var parsed = this.model.parse({
"reshared": {
"id": 45,
"target_type": "Post",
"target_id": 11,
"recipient_id": 1,
"unread": true,
"created_at": "2015-10-27T19:56:30.000Z",
"updated_at": "2015-10-27T19:56:30.000Z",
"note_html": "<html/>"
},
"type": "reshared"
});
expect(parsed).toEqual({
"type": "reshared",
"id": 45,
"target_type": "Post",
"target_id": 11,
"recipient_id": 1,
"unread": true,
"created_at": "2015-10-27T19:56:30.000Z",
"updated_at": "2015-10-27T19:56:30.000Z",
"note_html": "<html/>"
});
});
});
describe("setRead", function() {
it("calls setUnreadStatus with 'false'", function() {
spyOn(app.models.Notification.prototype, "setUnreadStatus");
new app.models.Notification({"reshared": {}, "type": "reshared"}).setRead();
expect(app.models.Notification.prototype.setUnreadStatus).toHaveBeenCalledWith(false);
});
});
describe("setUnread", function() {
it("calls setUnreadStatus with 'true'", function() {
spyOn(app.models.Notification.prototype, "setUnreadStatus");
new app.models.Notification({"reshared": {}, "type": "reshared"}).setUnread();
expect(app.models.Notification.prototype.setUnreadStatus).toHaveBeenCalledWith(true);
});
});
describe("setUnreadStatus", function() {
beforeEach(function() {
this.target = new app.models.Notification({"reshared": {id: 16}, "type": "reshared"});
spyOn(app.models.Notification.prototype, "set").and.callThrough();
});
it("calls calls ajax with correct parameters and sets 'unread' attribute", function() {
this.target.setUnreadStatus(true);
jasmine.Ajax.requests.mostRecent().respondWith({status: 200, responseText: '{"guid": 16, "unread": true}'});
var call = jasmine.Ajax.requests.mostRecent();
expect(call.url).toBe("/notifications/16");
/* eslint-disable camelcase */
expect(call.params).toEqual("set_unread=true");
/* eslint-enable camelcase */
expect(call.method).toEqual("PUT");
expect(app.models.Notification.prototype.set).toHaveBeenCalledWith("unread", true);
});
});
});

View file

@ -6,6 +6,7 @@ describe("app.views.Header", function() {
spec.loadFixture("aspects_index"); spec.loadFixture("aspects_index");
gon.appConfig = {settings: {podname: "MyPod"}}; gon.appConfig = {settings: {podname: "MyPod"}};
app.notificationsCollection = new app.collections.Notifications();
this.view = new app.views.Header().render(); this.view = new app.views.Header().render();
}); });

View file

@ -1,125 +1,74 @@
describe("app.views.NotificationDropdown", function() { describe("app.views.NotificationDropdown", function() {
beforeEach(function (){ beforeEach(function() {
spec.loadFixture("notifications"); spec.loadFixture("notifications");
gon.appConfig = {settings: {podname: "MyPod"}}; gon.appConfig = {settings: {podname: "MyPod"}};
this.header = new app.views.Header(); this.header = new app.views.Header();
$("header").prepend(this.header.el); $("header").prepend(this.header.el);
loginAs({guid: "foo"}); loginAs({guid: "foo"});
this.header.render(); this.header.render();
this.view = new app.views.NotificationDropdown({el: "#notification-dropdown"}); this.collection = new app.collections.Notifications();
this.view = new app.views.NotificationDropdown({el: "#notification-dropdown", collection: this.collection});
}); });
context("showDropdown", function(){ describe("bindCollectionEvents", function() {
it("Calls resetParam()", function(){ beforeEach(function() {
spyOn(this.view, "resetParams"); this.view.collection.off("pushFront");
this.view.showDropdown(); this.view.collection.off("pushBack");
expect(this.view.resetParams).toHaveBeenCalled(); this.view.collection.off("finishedLoading");
spyOn(this.view, "onPushFront");
spyOn(this.view, "onPushBack");
spyOn(this.view, "finishLoading");
}); });
it("Calls updateScrollbar()", function(){
it("binds collection events", function() {
this.view.bindCollectionEvents();
this.collection.trigger("pushFront");
this.collection.trigger("pushBack");
this.collection.trigger("finishedLoading");
expect(this.view.onPushFront).toHaveBeenCalled();
expect(this.view.onPushBack).toHaveBeenCalled();
expect(this.view.finishLoading).toHaveBeenCalled();
});
});
describe("showDropdown", function() {
it("Calls updateScrollbar", function() {
spyOn(this.view, "updateScrollbar"); spyOn(this.view, "updateScrollbar");
this.view.showDropdown(); this.view.showDropdown();
expect(this.view.updateScrollbar).toHaveBeenCalled(); expect(this.view.updateScrollbar).toHaveBeenCalled();
}); });
it("Changes CSS", function(){ it("Changes CSS", function() {
expect($("#notification-dropdown")).not.toHaveClass("dropdown-open"); expect($("#notification-dropdown")).not.toHaveClass("dropdown-open");
this.view.showDropdown(); this.view.showDropdown();
expect($("#notification-dropdown")).toHaveClass("dropdown-open"); expect($("#notification-dropdown")).toHaveClass("dropdown-open");
}); });
it("Calls getNotifications()", function(){ it("Calls collection#fetch", function() {
spyOn(this.view, "getNotifications"); spyOn(this.collection, "fetch");
this.view.showDropdown(); this.view.showDropdown();
expect(this.view.getNotifications).toHaveBeenCalled(); expect(this.collection.fetch).toHaveBeenCalled();
}); });
}); });
context("dropdownScroll", function(){ describe("dropdownScroll", function() {
it("Calls getNotifications if is at the bottom and has more notifications to load", function(){ it("Calls collection#fetchMore if it is at the bottom", function() {
this.view.isBottom = function(){ return true; }; this.view.isBottom = function() { return true; };
this.view.hasMoreNotifs = true; spyOn(this.collection, "fetchMore");
spyOn(this.view, "getNotifications");
this.view.dropdownScroll(); this.view.dropdownScroll();
expect(this.view.getNotifications).toHaveBeenCalled(); expect(this.collection.fetchMore).toHaveBeenCalled();
}); });
it("Doesn't call getNotifications if is not at the bottom", function(){ it("Doesn't call collection#fetchMore if it is not at the bottom", function() {
this.view.isBottom = function(){ return false; }; this.view.isBottom = function() { return false; };
this.view.hasMoreNotifs = true; spyOn(this.collection, "fetchMore");
spyOn(this.view, "getNotifications");
this.view.dropdownScroll(); this.view.dropdownScroll();
expect(this.view.getNotifications).not.toHaveBeenCalled(); expect(this.collection.fetchMore).not.toHaveBeenCalled();
});
it("Doesn't call getNotifications if is not at the bottom", function(){
this.view.isBottom = function(){ return true; };
this.view.hasMoreNotifs = false;
spyOn(this.view, "getNotifications");
this.view.dropdownScroll();
expect(this.view.getNotifications).not.toHaveBeenCalled();
}); });
}); });
context("getNotifications", function(){ describe("updateScrollbar", function() {
it("Has more notifications", function(){ it("Initializes perfectScrollbar", function() {
var response = ["", "", "", "", ""];
spyOn($, "getJSON").and.callFake(function(url, callback){ callback(response); });
this.view.getNotifications();
expect(this.view.hasMoreNotifs).toBe(true);
});
it("Has no more notifications", function(){
spyOn($, "getJSON").and.callFake(function(url, callback){ callback([]); });
this.view.getNotifications();
expect(this.view.hasMoreNotifs).toBe(false);
});
it("Correctly sets the next page", function(){
spyOn($, "getJSON").and.callFake(function(url, callback){ callback([]); });
expect(typeof this.view.nextPage).toBe("undefined");
this.view.getNotifications();
expect(this.view.nextPage).toBe(3);
});
it("Increase the page count", function(){
var response = ["", "", "", "", ""];
spyOn($, "getJSON").and.callFake(function(url, callback){ callback(response); });
this.view.getNotifications();
expect(this.view.nextPage).toBe(3);
this.view.getNotifications();
expect(this.view.nextPage).toBe(4);
});
it("Calls renderNotifications()", function(){
spyOn($, "getJSON").and.callFake(function(url, callback){ callback([]); });
spyOn(this.view, "renderNotifications");
this.view.getNotifications();
expect(this.view.renderNotifications).toHaveBeenCalled();
});
it("Adds the notifications to this.notifications", function(){
var response = ["", "", "", "", ""];
this.view.notifications.length = 0;
spyOn($, "getJSON").and.callFake(function(url, callback){ callback(response); });
this.view.getNotifications();
expect(this.view.notifications).toEqual(response);
});
});
context("renderNotifications", function(){
it("Removes the previous notifications", function(){
this.view.dropdownNotifications.append("<div class=\"media stream-element\">Notification</div>");
expect(this.view.dropdownNotifications.find(".media.stream-element").length).toBe(1);
this.view.renderNotifications();
expect(this.view.dropdownNotifications.find(".media.stream-element").length).toBe(0);
});
it("Calls hideAjaxLoader()", function(){
spyOn(this.view, "hideAjaxLoader");
this.view.renderNotifications();
expect(this.view.hideAjaxLoader).toHaveBeenCalled();
});
it("Calls updateScrollbar()", function(){
spyOn(this.view, "updateScrollbar");
this.view.renderNotifications();
expect(this.view.updateScrollbar).toHaveBeenCalled();
});
});
context("updateScrollbar", function() {
it("Initializes perfectScrollbar", function(){
this.view.perfectScrollbarInitialized = false; this.view.perfectScrollbarInitialized = false;
spyOn($.fn, "perfectScrollbar"); spyOn($.fn, "perfectScrollbar");
this.view.updateScrollbar(); this.view.updateScrollbar();
@ -128,7 +77,7 @@ describe("app.views.NotificationDropdown", function() {
expect(this.view.perfectScrollbarInitialized).toBeTruthy(); expect(this.view.perfectScrollbarInitialized).toBeTruthy();
}); });
it("Updates perfectScrollbar", function(){ it("Updates perfectScrollbar", function() {
this.view.perfectScrollbarInitialized = true; this.view.perfectScrollbarInitialized = true;
this.view.dropdownNotifications.perfectScrollbar(); this.view.dropdownNotifications.perfectScrollbar();
spyOn($.fn, "perfectScrollbar"); spyOn($.fn, "perfectScrollbar");
@ -139,8 +88,8 @@ describe("app.views.NotificationDropdown", function() {
}); });
}); });
context("destroyScrollbar", function() { describe("destroyScrollbar", function() {
it("destroys perfectScrollbar", function(){ it("destroys perfectScrollbar", function() {
this.view.perfectScrollbarInitialized = true; this.view.perfectScrollbarInitialized = true;
this.view.dropdownNotifications.perfectScrollbar(); this.view.dropdownNotifications.perfectScrollbar();
spyOn($.fn, "perfectScrollbar"); spyOn($.fn, "perfectScrollbar");
@ -150,7 +99,7 @@ describe("app.views.NotificationDropdown", function() {
expect(this.view.perfectScrollbarInitialized).toBeFalsy(); expect(this.view.perfectScrollbarInitialized).toBeFalsy();
}); });
it("doesn't destroy perfectScrollbar if it isn't initialized", function(){ it("doesn't destroy perfectScrollbar if it isn't initialized", function() {
this.view.perfectScrollbarInitialized = false; this.view.perfectScrollbarInitialized = false;
spyOn($.fn, "perfectScrollbar"); spyOn($.fn, "perfectScrollbar");
this.view.destroyScrollbar(); this.view.destroyScrollbar();

View file

@ -1,8 +1,36 @@
describe("app.views.Notifications", function(){ describe("app.views.Notifications", function() {
beforeEach(function() {
this.collection = new app.collections.Notifications();
this.collection.fetch();
jasmine.Ajax.requests.mostRecent().respondWith({
status: 200,
responseText: spec.readFixture("notifications_collection")
});
});
context("on the notifications page", function() { context("on the notifications page", function() {
beforeEach(function() { beforeEach(function() {
spec.loadFixture("notifications"); spec.loadFixture("notifications");
this.view = new app.views.Notifications({el: "#notifications_container"}); this.view = new app.views.Notifications({el: "#notifications_container", collection: this.collection});
});
describe("bindCollectionEvents", function() {
beforeEach(function() {
this.view.collection.off("change");
this.view.collection.off("update");
spyOn(this.view, "onChangedUnreadStatus");
spyOn(this.view, "updateView");
});
it("binds collection events", function() {
this.view.bindCollectionEvents();
this.collection.trigger("change");
this.collection.trigger("update");
expect(this.view.onChangedUnreadStatus).toHaveBeenCalled();
expect(this.view.updateView).toHaveBeenCalled();
});
}); });
describe("mark read", function() { describe("mark read", function() {
@ -11,11 +39,11 @@ describe("app.views.Notifications", function(){
this.guid = this.unreadN.data("guid"); this.guid = this.unreadN.data("guid");
}); });
it("calls 'setRead'", function() { it("calls collection's 'setRead'", function() {
spyOn(this.view, "setRead"); spyOn(this.collection, "setRead");
this.unreadN.find(".unread-toggle").trigger("click"); this.unreadN.find(".unread-toggle").trigger("click");
expect(this.view.setRead).toHaveBeenCalledWith(this.guid); expect(this.collection.setRead).toHaveBeenCalledWith(this.guid);
}); });
}); });
@ -25,11 +53,11 @@ describe("app.views.Notifications", function(){
this.guid = this.readN.data("guid"); this.guid = this.readN.data("guid");
}); });
it("calls 'setUnread'", function() { it("calls collection's 'setUnread'", function() {
spyOn(this.view, "setUnread"); spyOn(this.collection, "setUnread");
this.readN.find(".unread-toggle").trigger("click"); this.readN.find(".unread-toggle").trigger("click");
expect(this.view.setUnread).toHaveBeenCalledWith(this.guid); expect(this.collection.setUnread).toHaveBeenCalledWith(this.guid);
}); });
}); });
@ -40,42 +68,65 @@ describe("app.views.Notifications", function(){
this.type = this.readN.data("type"); this.type = this.readN.data("type");
}); });
it("changes the 'all notifications' count", function() { it("increases the 'all notifications' count", function() {
var badge = $(".list-group > a:eq(0) .badge"); var badge = $(".list-group > a:eq(0) .badge");
var count = parseInt(badge.text()); expect(parseInt(badge.text(), 10)).toBe(2);
this.view.updateView(this.guid, this.type, true); this.collection.unreadCount++;
expect(parseInt(badge.text())).toBe(count + 1); this.view.updateView();
expect(parseInt(badge.text(), 10)).toBe(3);
this.view.updateView(this.guid, this.type, false); this.view.updateView();
expect(parseInt(badge.text())).toBe(count); expect(parseInt(badge.text(), 10)).toBe(3);
}); });
it("changes the notification type count", function() { it("decreases the 'all notifications' count", function() {
var badge = $(".list-group > a:eq(0) .badge");
expect(parseInt(badge.text(), 10)).toBe(2);
this.collection.unreadCount--;
this.view.updateView();
expect(parseInt(badge.text(), 10)).toBe(1);
this.view.updateView();
expect(parseInt(badge.text(), 10)).toBe(1);
});
it("increases the notification type count", function() {
var badge = $(".list-group > a[data-type=" + this.type + "] .badge"); var badge = $(".list-group > a[data-type=" + this.type + "] .badge");
var count = parseInt(badge.text());
this.view.updateView(this.guid, this.type, true); expect(parseInt(badge.text(), 10)).toBe(1);
expect(parseInt(badge.text())).toBe(count + 1);
this.view.updateView(this.guid, this.type, false); this.collection.unreadCountByType[this.type]++;
expect(parseInt(badge.text())).toBe(count); this.view.updateView();
expect(parseInt(badge.text(), 10)).toBe(2);
this.view.updateView();
expect(parseInt(badge.text(), 10)).toBe(2);
}); });
it("toggles the unread class and changes the title", function() { it("decreases the notification type count", function() {
this.view.updateView(this.readN.data("guid"), this.readN.data("type"), true); var badge = $(".list-group > a[data-type=" + this.type + "] .badge");
expect(this.readN.hasClass("unread")).toBeTruthy();
expect(this.readN.hasClass("read")).toBeFalsy();
expect(this.readN.find(".unread-toggle .entypo-eye").attr("data-original-title")).toBe(
Diaspora.I18n.t("notifications.mark_read")
);
this.view.updateView(this.readN.data("guid"), this.readN.data("type"), false); expect(parseInt(badge.text(), 10)).toBe(1);
expect(this.readN.hasClass("read")).toBeTruthy();
expect(this.readN.hasClass("unread")).toBeFalsy(); this.collection.unreadCountByType[this.type]--;
expect(this.readN.find(".unread-toggle .entypo-eye").attr("data-original-title")).toBe( this.view.updateView();
Diaspora.I18n.t("notifications.mark_unread") expect(parseInt(badge.text(), 10)).toBe(0);
);
this.view.updateView();
expect(parseInt(badge.text(), 10)).toBe(0);
});
it("hides badge count when notification count is zero", function() {
Object.keys(this.collection.unreadCountByType).forEach(function(notificationType) {
this.collection.unreadCountByType[notificationType] = 0;
}.bind(this));
this.collection.unreadCount = 0;
this.view.updateView();
expect($("a .badge")).toHaveClass("hidden");
}); });
context("with a header", function() { context("with a header", function() {
@ -84,6 +135,7 @@ describe("app.views.Notifications", function(){
loginAs({name: "alice", avatar: {small: "http://avatar.com/photo.jpg"}, notifications_count: 2, guid: "foo"}); loginAs({name: "alice", avatar: {small: "http://avatar.com/photo.jpg"}, notifications_count: 2, guid: "foo"});
/* jshint camelcase: true */ /* jshint camelcase: true */
gon.appConfig = {settings: {podname: "MyPod"}}; gon.appConfig = {settings: {podname: "MyPod"}};
app.notificationsCollection = this.collection;
this.header = new app.views.Header(); this.header = new app.views.Header();
$("header").prepend(this.header.el); $("header").prepend(this.header.el);
this.header.render(); this.header.render();
@ -92,30 +144,77 @@ describe("app.views.Notifications", function(){
it("changes the header notifications count", function() { it("changes the header notifications count", function() {
var badge1 = $(".notifications-link:eq(0) .badge"); var badge1 = $(".notifications-link:eq(0) .badge");
var badge2 = $(".notifications-link:eq(1) .badge"); var badge2 = $(".notifications-link:eq(1) .badge");
var count = parseInt(badge1.text(), 10);
this.view.updateView(this.guid, this.type, true); expect(parseInt(badge1.text(), 10)).toBe(this.collection.unreadCount);
expect(parseInt(badge1.text(), 10)).toBe(count + 1); expect(parseInt(badge2.text(), 10)).toBe(this.collection.unreadCount);
this.view.updateView(this.guid, this.type, false); this.collection.unreadCount++;
expect(parseInt(badge1.text(), 10)).toBe(count); this.view.updateView();
expect(parseInt(badge1.text(), 10)).toBe(this.collection.unreadCount);
this.view.updateView(this.guid, this.type, true); this.view.updateView();
expect(parseInt(badge2.text(), 10)).toBe(count + 1); expect(parseInt(badge2.text(), 10)).toBe(this.collection.unreadCount);
});
this.view.updateView(this.guid, this.type, false); it("disables the mark-all-read-link button", function() {
expect(parseInt(badge2.text(), 10)).toBe(count); expect($("a#mark-all-read-link")).not.toHaveClass("disabled");
this.collection.unreadCount = 0;
this.view.updateView();
expect($("a#mark-all-read-link")).toHaveClass("disabled");
}); });
}); });
}); });
describe("markAllRead", function() { describe("markAllRead", function() {
it("calls setRead for each unread notification", function(){ it("calls collection#setAllRead", function() {
spyOn(this.view, "setRead"); spyOn(this.collection, "setAllRead");
this.view.markAllRead(); this.view.markAllRead();
expect(this.view.setRead).toHaveBeenCalledWith(this.view.$(".stream-element.unread").eq(0).data("guid")); expect(this.collection.setAllRead).toHaveBeenCalled();
this.view.markAllRead(); });
expect(this.view.setRead).toHaveBeenCalledWith(this.view.$(".stream-element.unread").eq(1).data("guid")); });
describe("onChangedUnreadStatus", function() {
beforeEach(function() {
this.modelRead = new app.models.Notification({});
this.modelRead.set("unread", false);
this.modelRead.guid = $(".stream-element.unread").first().data("guid");
this.modelUnread = new app.models.Notification({});
this.modelUnread.set("unread", true);
this.modelUnread.guid = $(".stream-element.read").first().data("guid");
});
it("Adds the unread class and changes the title", function() {
var unreadEl = $(".stream-element[data-guid=" + this.modelUnread.guid + "]");
expect(unreadEl.hasClass("read")).toBeTruthy();
expect(unreadEl.hasClass("unread")).toBeFalsy();
expect(unreadEl.find(".unread-toggle .entypo-eye").attr("data-original-title")).toBe(
Diaspora.I18n.t("notifications.mark_unread")
);
this.view.onChangedUnreadStatus(this.modelUnread);
expect(unreadEl.hasClass("unread")).toBeTruthy();
expect(unreadEl.hasClass("read")).toBeFalsy();
expect(unreadEl.find(".unread-toggle .entypo-eye").attr("data-original-title")).toBe(
Diaspora.I18n.t("notifications.mark_read")
);
});
it("Removes the unread class and changes the title", function() {
var readEl = $(".stream-element[data-guid=" + this.modelRead.guid + "]");
expect(readEl.hasClass("unread")).toBeTruthy();
expect(readEl.hasClass("read")).toBeFalsy();
expect(readEl.find(".unread-toggle .entypo-eye").attr("data-original-title")).toBe(
Diaspora.I18n.t("notifications.mark_read")
);
this.view.onChangedUnreadStatus(this.modelRead);
expect(readEl.hasClass("read")).toBeTruthy();
expect(readEl.hasClass("unread")).toBeFalsy();
expect(readEl.find(".unread-toggle .entypo-eye").attr("data-original-title")).toBe(
Diaspora.I18n.t("notifications.mark_unread")
);
}); });
}); });
}); });
@ -123,47 +222,32 @@ describe("app.views.Notifications", function(){
context("on the contacts page", function() { context("on the contacts page", function() {
beforeEach(function() { beforeEach(function() {
spec.loadFixture("aspects_manage"); spec.loadFixture("aspects_manage");
this.view = new app.views.Notifications({el: "#notifications_container"}); this.view = new app.views.Notifications({el: "#notifications_container", collection: this.collection});
/* jshint camelcase: false */ /* jshint camelcase: false */
loginAs({name: "alice", avatar: {small: "http://avatar.com/photo.jpg"}, notifications_count: 2, guid: "foo"}); loginAs({name: "alice", avatar: {small: "http://avatar.com/photo.jpg"}, notifications_count: 2, guid: "foo"});
/* jshint camelcase: true */ /* jshint camelcase: true */
gon.appConfig = {settings: {podname: "MyPod"}}; gon.appConfig = {settings: {podname: "MyPod"}};
app.notificationsCollection = this.collection;
this.header = new app.views.Header(); this.header = new app.views.Header();
$("header").prepend(this.header.el); $("header").prepend(this.header.el);
this.header.render(); this.header.render();
}); });
describe("updateView", function() { describe("updateView", function() {
it("changes the header notifications count", function() {
var badge1 = $(".notifications-link:eq(0) .badge");
var badge2 = $(".notifications-link:eq(1) .badge");
var count = parseInt(badge1.text(), 10);
this.view.updateView(this.guid, this.type, true);
expect(parseInt(badge1.text(), 10)).toBe(count + 1);
this.view.updateView(this.guid, this.type, false);
expect(parseInt(badge1.text(), 10)).toBe(count);
this.view.updateView(this.guid, this.type, true);
expect(parseInt(badge2.text(), 10)).toBe(count + 1);
this.view.updateView(this.guid, this.type, false);
expect(parseInt(badge2.text(), 10)).toBe(count);
});
it("doesn't change the contacts count", function() { it("doesn't change the contacts count", function() {
expect($("#aspect_nav .badge").length).toBeGreaterThan(0); expect($("#aspect_nav .badge").length).toBeGreaterThan(0);
$("#aspect_nav .badge").each(function(index, el) { $("#aspect_nav .badge").each(function(index, el) {
$(el).text(index + 1337); $(el).text(index + 1337);
}); });
this.view.updateView(this.guid, this.type, true); this.view.updateView();
$("#aspect_nav .badge").each(function(index, el) { $("#aspect_nav .badge").each(function(index, el) {
expect(parseInt($(el).text(), 10)).toBe(index + 1337); expect(parseInt($(el).text(), 10)).toBe(index + 1337);
}); });
this.view.updateView(this.guid, this.type, false); this.collection.unreadCount++;
this.view.updateView();
$("#aspect_nav .badge").each(function(index, el) { $("#aspect_nav .badge").each(function(index, el) {
expect(parseInt($(el).text(), 10)).toBe(index + 1337); expect(parseInt($(el).text(), 10)).toBe(index + 1337);
}); });