Add proper QUnit tests

Change-Id: I7a4abf068a32d9cf65eb464eb036f72f319c54ff
This commit is contained in:
Moriel Schottlender 2016-09-15 17:05:04 -07:00
parent a4296ad768
commit 7b4bb11c2a
15 changed files with 1354 additions and 273 deletions

View file

@ -197,7 +197,8 @@ $wgResourceModules += array(
),
'dependencies' => array(
'oojs',
'ext.echo.api'
'ext.echo.api', // This is required by the UnreadNotificationCounter
'moment',
),
'messages' => array(
'echo-api-failure',

View file

@ -20,6 +20,7 @@
this.foreign = true;
this.source = null;
this.count = config.count || 0;
this.modelName = config.modelName || 'xwiki';
this.list = new mw.echo.dm.NotificationGroupsList();

View file

@ -56,7 +56,6 @@
this.foreign = !!config.foreign;
this.bundled = !!config.bundled;
this.source = config.source || '';
this.modelName = config.modelName || 'local';
this.iconType = config.iconType;
this.iconURL = config.iconURL;

View file

@ -30,7 +30,7 @@
this.name = config.name || 'local';
this.source = config.source || 'local';
this.sourceURL = config.sourceURL;
this.sourceURL = config.sourceURL || '';
this.title = config.title || '';
this.fallbackTimestamp = config.timestamp || 0;
@ -68,7 +68,7 @@
/**
* @event update
* @param {mw.echo.dm.NotificationItem} items Current items in the list
* @param {mw.echo.dm.NotificationItem[]} items Current items in the list
*
* The list has been updated
*/
@ -181,15 +181,6 @@
return this.source;
};
/**
* Get the name associated with this list.
*
* @return {string} List name
*/
mw.echo.dm.NotificationsList.prototype.getName = function () {
return this.name;
};
/**
* Get the source article url associated with this list.
*

View file

@ -20,7 +20,7 @@
OO.EventEmitter.call( this );
this.pagesContinue = [];
this.itemsPerPage = this.itemsPerPage || 25;
this.itemsPerPage = config.itemsPerPage || 25;
this.currentPageItemCount = config.currentPageItemCount || this.itemsPerPage;
// Set initial page
@ -132,7 +132,7 @@
* @return {string} Previous page continue value
*/
mw.echo.dm.PaginationModel.prototype.getPrevPageContinue = function () {
return this.pagesContinue[ this.currPageIndex - 1 ];
return this.pagesContinue[ this.currPageIndex - 1 ] || '';
};
/**
@ -141,7 +141,7 @@
* @return {string} Current page continue value
*/
mw.echo.dm.PaginationModel.prototype.getCurrPageContinue = function () {
return this.pagesContinue[ this.currPageIndex ];
return this.pagesContinue[ this.currPageIndex ] || '';
};
/**
@ -150,7 +150,7 @@
* @return {string} Next page continue value
*/
mw.echo.dm.PaginationModel.prototype.getNextPageContinue = function () {
return this.pagesContinue[ this.currPageIndex + 1 ];
return this.pagesContinue[ this.currPageIndex + 1 ] || '';
};
/**

View file

@ -0,0 +1,119 @@
( function ( mw ) {
QUnit.module( 'ext.echo.dm - mw.echo.dm.BundleNotificationItem' );
QUnit.test( 'Constructing the model', function ( assert ) {
var bundledItems = [
new mw.echo.dm.NotificationItem( 0, { read: false, seen: false, timestamp: '201601010000' } ),
new mw.echo.dm.NotificationItem( 1, { read: false, seen: false, timestamp: '201601010100' } ),
new mw.echo.dm.NotificationItem( 2, { read: false, seen: true, timestamp: '201601010200' } ),
new mw.echo.dm.NotificationItem( 3, { read: false, seen: true, timestamp: '201601010300' } ),
new mw.echo.dm.NotificationItem( 4, { read: false, seen: true, timestamp: '201601010400' } )
],
bundle = new mw.echo.dm.BundleNotificationItem(
100,
bundledItems,
{
modelName: 'foo'
}
);
assert.equal(
bundle.getCount(),
5,
'Bundled items added to internal list'
);
assert.equal(
bundle.getName(),
'foo',
'Bundle name stored'
);
assert.deepEqual(
bundle.getAllIds(),
[ 4, 3, 2, 1, 0 ],
'All ids present'
);
assert.equal(
bundle.isRead(),
false,
'Bundle with all unread items is unread'
);
assert.equal(
bundle.hasUnseen(),
true,
'Bundle has unseen items'
);
assert.deepEqual(
( function () {
var findItems = bundle.findByIds( [ 1, 4 ] );
return findItems.map( function ( item ) {
return item.getId();
} );
} )(),
[ 4, 1 ],
'findByIds fetches correct items in the default sorting order'
);
} );
QUnit.test( 'Managing a list of items', function ( assert ) {
var i,
bundledItems = [
new mw.echo.dm.NotificationItem( 0, { read: false, seen: false, timestamp: '201601010000' } ),
new mw.echo.dm.NotificationItem( 1, { read: false, seen: false, timestamp: '201601010100' } ),
new mw.echo.dm.NotificationItem( 2, { read: false, seen: true, timestamp: '201601010200' } ),
new mw.echo.dm.NotificationItem( 3, { read: false, seen: true, timestamp: '201601010300' } ),
new mw.echo.dm.NotificationItem( 4, { read: false, seen: true, timestamp: '201601010400' } )
],
bundle = new mw.echo.dm.BundleNotificationItem(
100,
bundledItems,
{
name: 'foo'
}
);
assert.equal(
bundle.hasUnseen(),
true,
'Bundle has unseen'
);
// Mark all items as seen
for ( i = 0; i < bundledItems.length; i++ ) {
bundledItems[ i ].toggleSeen( true );
}
assert.equal(
bundle.hasUnseen(),
false,
'Bundle does not have unseen after all items marked as seen'
);
assert.equal(
bundle.isRead(),
false,
'Bundle is unread'
);
// Mark one item as read
bundledItems[ 0 ].toggleRead( true );
assert.equal(
bundle.isRead(),
false,
'Bundle is still unread if it has some unread items'
);
// Mark all items as read
for ( i = 0; i < bundledItems.length; i++ ) {
bundledItems[ i ].toggleRead( true );
}
assert.equal(
bundle.isRead(),
true,
'Bundle is marked as read if all items are read'
);
} );
} )( mediaWiki );

View file

@ -0,0 +1,311 @@
( function ( mw, $ ) {
var defaults = {
getModelName: 'xwiki',
getSourceNames: [],
getCount: 0,
hasUnseen: false,
getItems: [],
isEmpty: true
};
QUnit.module( 'ext.echo.dm - mw.echo.dm.CrossWikiNotificationItem' );
QUnit.test( 'Constructing the model', function ( assert ) {
var i, method, model,
cases = [
{
params: {
id: -1,
config: {}
},
expected: defaults,
msg: 'Default values'
},
{
params: {
id: -1,
config: { modelName: 'foo' }
},
expected: $.extend( true, {}, defaults, {
getModelName: 'foo'
} ),
msg: 'Overriding model name'
},
{
params: {
id: -1,
config: { count: 10 }
},
expected: $.extend( true, {}, defaults, {
getCount: 10
} ),
msg: 'Overriding model count'
}
];
for ( i = 0; i < cases.length; i++ ) {
model = new mw.echo.dm.CrossWikiNotificationItem(
cases[ i ].params.id,
cases[ i ].params.config
);
for ( method in defaults ) {
assert.deepEqual(
// Method
model[ method ](),
// Expected value
cases[ i ].expected[ method ],
cases[ i ].msg + ' (' + method + ')'
);
}
}
} );
QUnit.test( 'Managing notification lists', function ( assert ) {
var i, j,
model = new mw.echo.dm.CrossWikiNotificationItem( 1 ),
groupDefinitions = [
{
name: 'foo',
sourceData: {
title: 'Foo Wiki',
base: 'http://foo.wiki.sample/$1'
},
items: [
new mw.echo.dm.NotificationItem( 0, { source: 'foo', read: false, seen: false, timestamp: '201601010100' } ),
new mw.echo.dm.NotificationItem( 1, { source: 'foo', read: false, seen: false, timestamp: '201601010200' } ),
new mw.echo.dm.NotificationItem( 2, { source: 'foo', read: false, seen: false, timestamp: '201601010300' } )
]
},
{
name: 'bar',
sourceData: {
title: 'Bar Wiki',
base: 'http://bar.wiki.sample/$1'
},
items: [
new mw.echo.dm.NotificationItem( 3, { source: 'bar', read: false, seen: false, timestamp: '201601020000' } ),
new mw.echo.dm.NotificationItem( 4, { source: 'bar', read: false, seen: false, timestamp: '201601020100' } ),
new mw.echo.dm.NotificationItem( 5, { source: 'bar', read: false, seen: false, timestamp: '201601020200' } ),
new mw.echo.dm.NotificationItem( 6, { source: 'bar', read: false, seen: false, timestamp: '201601020300' } )
]
},
{
name: 'baz',
sourceData: {
title: 'Baz Wiki',
base: 'http://baz.wiki.sample/$1'
},
items: [
new mw.echo.dm.NotificationItem( 7, { source: 'baz', timestamp: '201601050100' } )
]
}
];
// Add groups to model
for ( i = 0; i < groupDefinitions.length; i++ ) {
model.getList().addGroup(
groupDefinitions[ i ].name,
groupDefinitions[ i ].sourceData,
groupDefinitions[ i ].items
);
}
assert.deepEqual(
model.getSourceNames(),
[ 'baz', 'bar', 'foo' ],
'Model source names exist in order'
);
assert.equal(
model.hasUnseen(),
true,
'hasUnseen is true if there are unseen items in any group'
);
// Mark all items as seen except one
for ( i = 0; i < groupDefinitions.length; i++ ) {
for ( j = 0; j < groupDefinitions[ i ].items.length; j++ ) {
groupDefinitions[ i ].items[ j ].toggleSeen( true );
}
}
groupDefinitions[ 0 ].items[ 0 ].toggleSeen( false );
assert.equal(
model.hasUnseen(),
true,
'hasUnseen is true even if only one item in one group is unseen'
);
groupDefinitions[ 0 ].items[ 0 ].toggleSeen( true );
assert.equal(
model.hasUnseen(),
false,
'hasUnseen is false if there are no unseen items in any of the groups'
);
// Discard group
model.getList().removeGroup( 'foo' );
assert.deepEqual(
model.getSourceNames(),
[ 'baz', 'bar' ],
'Group discarded successfully'
);
} );
QUnit.test( 'Update seen state', function ( assert ) {
var i, numUnseenItems, numAllItems,
model = new mw.echo.dm.CrossWikiNotificationItem( 1 ),
groupDefinitions = [
{
name: 'foo',
sourceData: {
title: 'Foo Wiki',
base: 'http://foo.wiki.sample/$1'
},
items: [
new mw.echo.dm.NotificationItem( 0, { source: 'foo', read: false, seen: false, timestamp: '201601010100' } ),
new mw.echo.dm.NotificationItem( 1, { source: 'foo', read: false, seen: false, timestamp: '201601010200' } ),
new mw.echo.dm.NotificationItem( 2, { source: 'foo', read: false, seen: false, timestamp: '201601010300' } )
]
},
{
name: 'bar',
sourceData: {
title: 'Bar Wiki',
base: 'http://bar.wiki.sample/$1'
},
items: [
new mw.echo.dm.NotificationItem( 3, { source: 'bar', read: false, seen: false, timestamp: '201601020000' } ),
new mw.echo.dm.NotificationItem( 4, { source: 'bar', read: false, seen: false, timestamp: '201601020100' } ),
new mw.echo.dm.NotificationItem( 5, { source: 'bar', read: false, seen: false, timestamp: '201601020200' } ),
new mw.echo.dm.NotificationItem( 6, { source: 'bar', read: false, seen: false, timestamp: '201601020300' } )
]
},
{
name: 'baz',
sourceData: {
title: 'Baz Wiki',
base: 'http://baz.wiki.sample/$1'
},
items: [
new mw.echo.dm.NotificationItem( 7, { source: 'baz', timestamp: '201601050100' } )
]
}
];
// Count all actual items
numAllItems = groupDefinitions.reduce( function ( prev, curr ) {
return prev + curr.items.length;
}, 0 );
// Add groups to model
for ( i = 0; i < groupDefinitions.length; i++ ) {
model.getList().addGroup(
groupDefinitions[ i ].name,
groupDefinitions[ i ].sourceData,
groupDefinitions[ i ].items
);
}
numUnseenItems = model.getItems().filter( function ( item ) {
return !item.isSeen();
} ).length;
assert.equal(
numUnseenItems,
numAllItems,
'Starting state: all items are unseen'
);
// Update seen time to be bigger than 'foo' but smaller than the other groups
model.updateSeenState( '201601010400' );
numUnseenItems = model.getItems().filter( function ( item ) {
return !item.isSeen();
} ).length;
assert.equal(
numUnseenItems,
numAllItems - groupDefinitions[ 0 ].items.length,
'Only some items are seen'
);
// Update seen time to be bigger than all
model.updateSeenState( '201701010000' );
numUnseenItems = model.getItems().filter( function ( item ) {
return !item.isSeen();
} ).length;
assert.equal(
numUnseenItems,
0,
'All items are seen'
);
} );
QUnit.test( 'Emit discard event', function ( assert ) {
var i,
results = [],
model = new mw.echo.dm.CrossWikiNotificationItem( -1 ),
groupDefinitions = [
{
name: 'foo',
sourceData: {
title: 'Foo Wiki',
base: 'http://foo.wiki.sample/$1'
},
items: [
new mw.echo.dm.NotificationItem( 0, { source: 'foo', read: false, seen: false, timestamp: '201601010100' } ),
new mw.echo.dm.NotificationItem( 1, { source: 'foo', read: false, seen: false, timestamp: '201601010200' } ),
new mw.echo.dm.NotificationItem( 2, { source: 'foo', read: false, seen: false, timestamp: '201601010300' } )
]
},
{
name: 'bar',
sourceData: {
title: 'Bar Wiki',
base: 'http://bar.wiki.sample/$1'
},
items: [
new mw.echo.dm.NotificationItem( 3, { source: 'bar', read: false, seen: false, timestamp: '201601020000' } ),
new mw.echo.dm.NotificationItem( 4, { source: 'bar', read: false, seen: false, timestamp: '201601020100' } ),
new mw.echo.dm.NotificationItem( 5, { source: 'bar', read: false, seen: false, timestamp: '201601020200' } ),
new mw.echo.dm.NotificationItem( 6, { source: 'bar', read: false, seen: false, timestamp: '201601020300' } )
]
},
{
name: 'baz',
sourceData: {
title: 'Baz Wiki',
base: 'http://baz.wiki.sample/$1'
},
items: [
new mw.echo.dm.NotificationItem( 7, { source: 'baz', timestamp: '201601050100' } )
]
}
];
// Add groups to model
for ( i = 0; i < groupDefinitions.length; i++ ) {
model.getList().addGroup(
groupDefinitions[ i ].name,
groupDefinitions[ i ].sourceData,
groupDefinitions[ i ].items
);
}
// Listen to event
model.on( 'discard', function ( name ) {
results.push( name );
} );
// Trigger
model.getList().removeGroup( 'foo' ); // [ 'foo' ]
// Empty a list
model.getList().getGroupByName( 'baz' ).discardItems( groupDefinitions[ 2 ].items ); // [ 'foo', 'baz' ]
assert.deepEqual(
results,
[ 'foo', 'baz' ],
'Discard event emitted'
);
} );
} )( mediaWiki, jQuery );

View file

@ -0,0 +1,110 @@
( function ( mw, $ ) {
var defaultValues = {
getReadState: 'all'
};
QUnit.module( 'ext.echo.dm - mw.echo.dm.FiltersModel' );
QUnit.test( 'Constructing the model', function ( assert ) {
var i, model, method,
cases = [
{
msg: 'Empty config',
config: {},
expected: defaultValues
},
{
msg: 'Readstate: unread',
config: {
readState: 'unread'
},
expected: $.extend( true, {}, defaultValues, {
getReadState: 'unread'
} )
},
{
msg: 'Readstate: read',
config: {
readState: 'read'
},
expected: $.extend( true, {}, defaultValues, {
getReadState: 'read'
} )
}
];
for ( i = 0; i < cases.length; i++ ) {
model = new mw.echo.dm.FiltersModel( cases[ i ].config );
for ( method in cases[ i ].expected ) {
assert.deepEqual(
// Run the method
model[ method ](),
// Expected value
cases[ i ].expected[ method ],
// Message
cases[ i ].msg + ' (' + method + ')'
);
}
}
} );
QUnit.test( 'Changing filters', function ( assert ) {
var model = new mw.echo.dm.FiltersModel();
assert.equal(
model.getReadState(),
'all',
'Initial value: all'
);
model.setReadState( 'unread' );
assert.equal(
model.getReadState(),
'unread',
'Changing state (unread)'
);
model.setReadState( 'read' );
assert.equal(
model.getReadState(),
'read',
'Changing state (read)'
);
model.setReadState( 'foo' );
assert.equal(
model.getReadState(),
'read',
'Ignoring invalid state (foo)'
);
} );
QUnit.test( 'Emitting update event', function ( assert ) {
var results = [],
model = new mw.echo.dm.FiltersModel();
// Listen to update event
model.on( 'update', function () {
results.push( model.getReadState() );
} );
// Trigger events
model.setReadState( 'read' ); // [ 'read' ]
model.setReadState( 'unread' ); // [ 'read', 'unread' ]
model.setReadState( 'unread' ); // (no change, no event) [ 'read', 'unread' ]
model.setReadState( 'all' ); // [ 'read', 'unread', 'all' ]
model.setReadState( 'foo' ); // (invalid value, no event) [ 'read', 'unread', 'all' ]
model.setReadState( 'unread' ); // [ 'read', 'unread', 'all', 'unread' ]
assert.deepEqual(
// Actual
results,
// Expected:
[ 'read', 'unread', 'all', 'unread' ],
// Message
'Update events emitted'
);
} );
} )( mediaWiki, jQuery );

View file

@ -0,0 +1,156 @@
( function ( mw ) {
QUnit.module( 'ext.echo.dm - mw.echo.dm.NotificationGroupsList' );
QUnit.test( 'Constructing the model', function ( assert ) {
var model = new mw.echo.dm.NotificationGroupsList();
assert.equal(
model.getTimestamp(),
0,
'Empty group has timestamp 0'
);
} );
QUnit.test( 'Managing lists', function ( assert ) {
var i, group,
model = new mw.echo.dm.NotificationGroupsList(),
groupDefinitions = [
{
name: 'foo',
sourceData: {
title: 'Foo Wiki',
base: 'http://foo.wiki.sample/$1'
},
items: [
new mw.echo.dm.NotificationItem( 0 ),
new mw.echo.dm.NotificationItem( 1 ),
new mw.echo.dm.NotificationItem( 2 )
]
},
{
name: 'bar',
sourceData: {
title: 'Bar Wiki',
base: 'http://bar.wiki.sample/$1'
},
items: [
new mw.echo.dm.NotificationItem( 3 ),
new mw.echo.dm.NotificationItem( 4 ),
new mw.echo.dm.NotificationItem( 5 ),
new mw.echo.dm.NotificationItem( 6 )
]
},
{
name: 'baz',
sourceData: {
title: 'Baz Wiki',
base: 'http://baz.wiki.sample/$1'
},
items: [
new mw.echo.dm.NotificationItem( 7 )
]
}
];
for ( i = 0; i < groupDefinitions.length; i++ ) {
model.addGroup(
groupDefinitions[ i ].name,
groupDefinitions[ i ].sourceData,
groupDefinitions[ i ].items
);
assert.equal(
model.getItemCount(),
i + 1,
'Group number increases after addGroup ("' + groupDefinitions[ i ].name + '")'
);
group = model.getGroupByName( groupDefinitions[ i ].name );
assert.equal(
group.getName(),
groupDefinitions[ i ].name,
'Group exists after addGroup ("' + groupDefinitions[ i ].name + '")'
);
}
// Remove group
model.removeGroup( groupDefinitions[ 0 ].name );
assert.equal(
model.getItemCount(),
groupDefinitions.length - 1,
'Group number decreased after removeGroup'
);
assert.equal(
model.getGroupByName( groupDefinitions[ 0 ] ),
null,
'Removed group is no longer in the list'
);
// Removing the last item from a group should remove the group
group = model.getGroupByName( 'baz' );
group.discardItems( groupDefinitions[ 2 ].items );
assert.equal(
model.getGroupByName( 'baz' ),
null,
'Empty group is no longer in the list'
);
} );
QUnit.test( 'Emitting discard event', function ( assert ) {
var group,
results = [],
model = new mw.echo.dm.NotificationGroupsList(),
groups = {
first: [
new mw.echo.dm.NotificationItem( 0 ),
new mw.echo.dm.NotificationItem( 1 ),
new mw.echo.dm.NotificationItem( 2 )
],
second: [
new mw.echo.dm.NotificationItem( 3 ),
new mw.echo.dm.NotificationItem( 4 ),
new mw.echo.dm.NotificationItem( 5 )
],
third: [
new mw.echo.dm.NotificationItem( 6 ),
new mw.echo.dm.NotificationItem( 7 )
],
fourth: [
new mw.echo.dm.NotificationItem( 8 ),
new mw.echo.dm.NotificationItem( 9 )
]
};
// Listen to the event
model
.on( 'discard', function ( group ) {
results.push( group.getName() );
} );
// Fill the list
for ( group in groups ) {
model.addGroup( group, {}, groups[ group ] );
}
// Trigger events
model.removeGroup( 'first' ); // [ 'first' ]
model.removeGroup( 'fourth' ); // [ 'first', 'fourth' ]
// Group doesn't exist, no change
model.removeGroup( 'first' ); // [ 'first', 'fourth' ]
// Discard of an item in a group (no event on the list model)
model.getGroupByName( 'third' ).discardItems( groups.third[ 0 ] ); // [ 'first', 'fourth' ]
// Discard of the last item in a group (trigger discard event on the list model)
model.getGroupByName( 'third' ).discardItems( groups.third[ 1 ] ); // [ 'first', 'fourth', 'third' ]
assert.deepEqual(
// Actual
results,
// Expected:
[ 'first', 'fourth', 'third' ],
// Message
'Discard events emitted'
);
} );
} )( mediaWiki );

View file

@ -0,0 +1,141 @@
( function ( mw, $ ) {
/*global moment:false */
var fakeData = {
type: 'alert',
read: true,
seen: true,
timestamp: '2016-09-14T23:21:56Z',
content: {
header: 'Your edit on <strong>Moai</strong> was reverted.',
compactHeader: 'Your edit on <strong>Moai</strong> was reverted.',
body: 'undo'
},
iconType: 'revert',
primaryUrl: 'http://dev.wiki.local.wmftest.net:8080/w/index.php?title=Moai&oldid=prev&diff=1978&markasread=2126',
secondaryUrls: [
{
url: 'http://dev.wiki.local.wmftest.net:8080/wiki/User:RandomUser',
label: 'RandomUser',
icon: 'userAvatar'
},
{
url: 'http://dev.wiki.local.wmftest.net:8080/wiki/Talk:Moai',
label: 'Moai',
tooltip: 'Talk:Moai',
icon: 'speechBubbles'
}
]
};
QUnit.module( 'ext.echo.dm - mw.echo.dm.NotificationItem' );
QUnit.test( 'Constructing items', function ( assert ) {
var i, j, itemModel, checkMethods,
defaultValues = {
getId: undefined,
getContentHeader: '',
getContentBody: '',
getCategory: '',
getType: 'message',
isRead: false,
isSeen: false,
isForeign: false,
isBundled: false,
getTimestamp: moment.utc().format( 'YYYY-MM-DD[T]HH:mm:ss[Z]' ),
getPrimaryUrl: undefined,
getIconURL: undefined,
getIconType: undefined,
getSecondaryUrls: [],
getModelName: 'local',
getAllIds: []
},
tests = [
{
msg: 'Empty data',
params: { id: 0, config: {} },
tests: 'all',
expected: $.extend( true, {}, defaultValues, { getId: 0, getAllIds: [ 0 ] } )
},
{
msg: 'Fake data',
params: { id: 999, config: fakeData },
tests: 'all',
expected: $.extend( true, {}, defaultValues, {
getId: 999,
getAllIds: [ 999 ],
getType: 'alert',
isRead: true,
isSeen: true,
getTimestamp: '2016-09-14T23:21:56Z',
getContentHeader: 'Your edit on <strong>Moai</strong> was reverted.',
getContentBody: 'undo',
getIconType: 'revert',
getPrimaryUrl: 'http://dev.wiki.local.wmftest.net:8080/w/index.php?title=Moai&oldid=prev&diff=1978&markasread=2126',
getSecondaryUrls: [
{
url: 'http://dev.wiki.local.wmftest.net:8080/wiki/User:RandomUser',
label: 'RandomUser',
icon: 'userAvatar'
},
{
url: 'http://dev.wiki.local.wmftest.net:8080/wiki/Talk:Moai',
label: 'Moai',
tooltip: 'Talk:Moai',
icon: 'speechBubbles'
}
]
} )
}
];
for ( i = 0; i < tests.length; i++ ) {
itemModel = new mw.echo.dm.NotificationItem( tests[ i ].params.id, tests[ i ].params.config );
checkMethods = tests[ i ].tests;
if ( tests[ i ].tests === 'all' ) {
checkMethods = Object.keys( defaultValues );
}
for ( j = 0; j < checkMethods.length; j++ ) {
assert.deepEqual(
// Run the method
itemModel[ checkMethods[ j ] ](),
// Expected result
tests[ i ].expected[ checkMethods[ j ] ],
// Message
tests[ i ].msg + ' (' + checkMethods[ j ] + ')'
);
}
}
} );
QUnit.test( 'Emitting update event', function ( assert ) {
var results = [],
itemModel = new mw.echo.dm.NotificationItem( 0, $.extend( true, {}, fakeData, { seen: false, read: false } ) );
// Listen to update event
itemModel.on( 'update', function () {
results.push( [
itemModel.isRead(),
itemModel.isSeen()
] );
} );
// Trigger events
itemModel.toggleSeen( true ); // [ [ false, true ] ]
itemModel.toggleSeen( true ); // [ [ false, true ] ] ( No change, event was not emitted )
itemModel.toggleRead( true ); // [ [ false, true ], [ true, true ] ]
itemModel.toggleRead( true ); // [ [ false, true ], [ true, true ] ] ( No change, event was not emitted )
itemModel.toggleRead( false ); // [ [ false, true ], [ true, true ], [ false, true ] ]
itemModel.toggleSeen( false ); // [ [ false, true ], [ true, true ], [ false, true ], [ false, false ] ]
itemModel.toggleRead( true ); // [ [ false, true ], [ true, true ], [ false, true ], [ false, false ], [ true, false ] ]
assert.deepEqual(
results,
// Expected:
[ [ false, true ], [ true, true ], [ false, true ], [ false, false ], [ true, false ] ],
'Read and seen changes produced "update" events'
);
} );
} )( mediaWiki, jQuery );

View file

@ -0,0 +1,189 @@
( function ( mw, $ ) {
var defaultValues = {
getAllItemIds: [],
getAllItemIdsByType: [],
getTitle: '',
getName: 'local',
getSource: 'local',
getSourceURL: '',
getTimestamp: 0,
getCount: 0,
hasUnseen: false,
isForeign: false
};
QUnit.module( 'ext.echo.dm - mw.echo.dm.NotificationsList' );
QUnit.test( 'Constructing the model', function ( assert ) {
var i, model, method,
cases = [
{
msg: 'Empty config',
config: {},
expected: defaultValues
},
{
msg: 'Prefilled data',
config: {
title: 'Some title',
name: 'local_demo',
source: 'hewiki',
sourceURL: 'http://he.wiki.local.wmftest.net:8080/wiki/$1',
timestamp: '20160916171300'
},
expected: $.extend( true, {}, defaultValues, {
getTitle: 'Some title',
getName: 'local_demo',
getSource: 'hewiki',
getSourceURL: 'http://he.wiki.local.wmftest.net:8080/wiki/$1',
getTimestamp: '20160916171300',
isForeign: true
} )
}
];
for ( i = 0; i < cases.length; i++ ) {
model = new mw.echo.dm.NotificationsList( cases[ i ].config );
for ( method in cases[ i ].expected ) {
assert.deepEqual(
// Run the method
model[ method ](),
// Expected value
cases[ i ].expected[ method ],
// Message
cases[ i ].msg + ' (' + method + ')'
);
}
}
} );
QUnit.test( 'Handling notification items', function ( assert ) {
var model = new mw.echo.dm.NotificationsList( { timestamp: '200101010000' } ),
items = [
new mw.echo.dm.NotificationItem( 0, { type: 'alert', timestamp: '201609190000', read: false, seen: false } ),
new mw.echo.dm.NotificationItem( 1, { type: 'message', timestamp: '201609190100', read: false, seen: true } ),
new mw.echo.dm.NotificationItem( 2, { type: 'alert', timestamp: '201609190200', read: true, seen: true } ),
new mw.echo.dm.NotificationItem( 3, { type: 'message', timestamp: '201609190300', read: true, seen: true } ),
new mw.echo.dm.NotificationItem( 4, { type: 'alert', timestamp: '201609190400', read: true, seen: true } ),
new mw.echo.dm.NotificationItem( 5, { type: 'message', timestamp: '201609190500', read: true, seen: false } )
];
assert.equal(
model.getCount(),
0,
'Model list starts empty'
);
assert.equal(
model.getTimestamp(),
'200101010000',
'Model timestamp is its default'
);
model.setItems( items );
assert.equal(
model.getCount(),
6,
'Item list setup'
);
assert.equal(
model.getTimestamp(),
'201609190100',
'Model timestamp is the latest unread item\'s timestamp'
);
assert.deepEqual(
model.getAllItemIds(),
[ 1, 0, 5, 4, 3, 2 ],
'getAllItemIds (sorted)'
);
assert.deepEqual(
[
model.getAllItemIdsByType( 'alert' ),
model.getAllItemIdsByType( 'message' )
],
[
[ 0, 4, 2 ],
[ 1, 5, 3 ]
],
'getAllItemIdsByType (sorted)'
);
assert.deepEqual(
model.findByIds( [ 1, 2 ] ),
[ items[ 1 ], items[ 2 ] ],
'findByIds'
);
// Change item state (trigger resort)
items[ 1 ].toggleRead( true );
items[ 3 ].toggleRead( false );
items[ 5 ].toggleSeen( true ); // Will not affect sorting order of the item
assert.deepEqual(
model.getAllItemIds(),
[ 3, 0, 5, 4, 2, 1 ],
'getAllItemIds (re-sorted)'
);
// Discard items
model.discardItems( [ items[ 5 ], items[ 2 ] ] );
assert.deepEqual(
model.getAllItemIds(),
[ 3, 0, 4, 1 ],
'getAllItemIds (discarded items)'
);
assert.deepEqual(
[
model.getAllItemIdsByType( 'alert' ),
model.getAllItemIdsByType( 'message' )
],
[
[ 0, 4 ],
[ 3, 1 ]
],
'getAllItemIdsByType (discarded items)'
);
} );
QUnit.test( 'Intercepting events', function ( assert ) {
var model = new mw.echo.dm.NotificationsList(),
result = [],
items = [
new mw.echo.dm.NotificationItem( 0, { timestamp: '201609190000', read: false, seen: false } ),
new mw.echo.dm.NotificationItem( 1, { timestamp: '201609190100', read: false, seen: true } ),
new mw.echo.dm.NotificationItem( 2, { timestamp: '201609190200', read: true, seen: true } ),
new mw.echo.dm.NotificationItem( 3, { timestamp: '201609190300', read: true, seen: true } ),
new mw.echo.dm.NotificationItem( 4, { timestamp: '201609190400', read: true, seen: true } ),
new mw.echo.dm.NotificationItem( 5, { timestamp: '201609190500', read: true, seen: true } )
];
// Listen to events
model
.on( 'update', function ( items ) {
result.push( 'update:' + items.length );
} )
.on( 'discard', function ( item ) {
result.push( 'discard:' + item.getId() );
} )
.on( 'itemUpdate', function ( item ) {
result.push( 'itemUpdate:' + item.getId() );
} );
// Set up and trigger events
model
.setItems( items ); // [ 'update:6' ]
model.discardItems( items[ items.length - 1 ] ); // [ 'update:6', 'discard:5' ]
items[ 0 ].toggleSeen( true ); // [ 'update:6', 'discard:5', 'itemUpdate:0' ]
items[ 1 ].toggleRead( true ); // [ 'update:6', 'discard:5', 'itemUpdate:0', 'itemUpdate:1' ]
assert.deepEqual(
// Actual
result,
// Expected:
[ 'update:6', 'discard:5', 'itemUpdate:0', 'itemUpdate:1' ],
// Message
'Events emitted correctly'
);
} );
} )( mediaWiki, jQuery );

View file

@ -0,0 +1,91 @@
( function ( mw, $ ) {
var defaultValues = {
getPageContinue: undefined,
getCurrPageIndex: 0,
getPrevPageContinue: '',
getCurrPageContinue: '',
getNextPageContinue: '',
hasPrevPage: false,
hasNextPage: false,
getCurrentPageItemCount: 25,
getItemsPerPage: 25
};
QUnit.module( 'ext.echo.dm - mw.echo.dm.PaginationModel' );
QUnit.test( 'Constructing the model', function ( assert ) {
var i, model, method,
cases = [
{
msg: 'Empty config',
config: {},
expected: defaultValues
},
{
msg: 'Overridng defaults',
config: {
pageNext: 'continueValNext|123',
itemsPerPage: 10
},
expected: $.extend( true, {}, defaultValues, {
getNextPageContinue: 'continueValNext|123',
hasNextPage: true,
getItemsPerPage: 10,
getCurrentPageItemCount: 10
} )
}
];
for ( i = 0; i < cases.length; i++ ) {
model = new mw.echo.dm.PaginationModel( cases[ i ].config );
for ( method in cases[ i ].expected ) {
assert.deepEqual(
// Run the method
model[ method ](),
// Expected value
cases[ i ].expected[ method ],
// Message
cases[ i ].msg + ' (' + method + ')'
);
}
}
} );
QUnit.test( 'Emitting update event', function ( assert ) {
var results = [],
model = new mw.echo.dm.PaginationModel();
// Listen to update event
model.on( 'update', function () {
results.push( [
model.getCurrPageIndex(),
model.hasNextPage()
] );
} );
// Trigger events
// Set up initial pages (first page is 0)
model.setPageContinue( 1, 'page2|2' ); // [ [ 0, true ] ]
model.setPageContinue( 2, 'page3|3' ); // [ [ 0, true ], [ 0, true ] ]
model.setPageContinue( 3, 'page4|4' ); // [ [ 0, true ], [ 0, true ], [ 0, true ] ]
model.forwards(); // [ [ 0, true ], [ 0, true ], [ 0, true ], [ 1, true ] ]
model.forwards(); // [ [ 0, true ], [ 0, true ], [ 0, true ], [ 1, true ], [ 2, true ] ]
model.forwards(); // [ [ 0, true ], [ 0, true ], [ 0, true ], [ 1, true ], [ 2, true ], [ 3, false ] ]
model.backwards(); // [ [ 0, true ], [ 0, true ], [ 0, true ], [ 1, true ], [ 2, true ], [ 3, false ], [ 2, true ] ]
model.setCurrentPageItemCount(); // [ [ 0, true ], [ 0, true ], [ 0, true ], [ 1, true ], [ 2, true ], [ 3, false ], [ 2, true ], [ 2, true ] ]
model.reset(); // [ [ 0, true ], [ 0, true ], [ 0, true ], [ 1, true ], [ 2, true ], [ 3, false ], [ 2, true ], [ 2, true ], [ 0, false ] ]
assert.deepEqual(
// Actual
results,
// Expected:
[ [ 0, true ], [ 0, true ], [ 0, true ], [ 1, true ], [ 2, true ], [ 3, false ], [ 2, true ], [ 2, true ], [ 0, false ] ],
// Message
'Update events emitted'
);
} );
} )( mediaWiki, jQuery );

View file

@ -0,0 +1,56 @@
( function ( mw ) {
QUnit.module( 'ext.echo.dm - mw.echo.dm.SeenTimeModel' );
QUnit.test( 'Constructing the model', function ( assert ) {
var model = new mw.echo.dm.SeenTimeModel();
assert.deepEqual(
model.getTypes(),
[ 'alert', 'message' ],
'Default model has both types'
);
} );
QUnit.test( 'Handling seenTime', function ( assert ) {
var model;
model = new mw.echo.dm.SeenTimeModel();
model.setSeenTime( '20160101010000' );
assert.deepEqual(
model.getSeenTime(),
'20160101010000',
'Model sets seen time for both types'
);
model = new mw.echo.dm.SeenTimeModel( { types: 'alert' } );
model.setSeenTime( '20160101010001' );
assert.deepEqual(
model.getSeenTime(),
'20160101010001',
'Alerts seen time model returns correct time'
);
} );
QUnit.test( 'Emitting update event', function ( assert ) {
var results = [],
model = new mw.echo.dm.SeenTimeModel();
// Attach a listener
model.on( 'update', function ( time ) {
results.push( time );
} );
// Trigger events
model.setSeenTime( '1' ); // [ '1' ]
model.setSeenTime( '2' ); // [ '1', '2' ]
model.setSeenTime( '2' ); // (no change, no event) [ '1', '2' ]
assert.deepEqual(
results,
[ '1', '2' ],
'Update event emitted'
);
} );
} )( mediaWiki );

View file

@ -0,0 +1,172 @@
( function ( mw ) {
// Mock partial API response we feed into the model
var sources = {
local: {
pages: [
{
ns: 2,
title: 'User:Admin',
unprefixed: 'Admin',
pages: [
'User:Admin',
'User talk:Admin',
null
],
count: 24
},
{
ns: 2,
title: 'User:RandomUser',
unprefixed: 'RandomUser',
pages: [
'User:RandomUser',
'User talk:RandomUser'
],
count: 6
},
{
ns: 0,
title: 'Moai',
unprefixed: 'Moai',
pages: [
'Moai',
'Talk:Moai'
],
count: 3
}
],
totalCount: 33,
source: {
title: 'LocalWiki',
url: 'http://dev.wiki.local.wmftest.net:8080/w/api.php',
base: 'http://dev.wiki.local.wmftest.net:8080/wiki/$1'
}
},
hewiki: {
pages: [
{
ns: 2,
title: 'Foo',
unprefixed: 'Foo',
pages: [
'Foo',
'Talk:Foo',
null
],
count: 10
},
{
ns: 0,
title: 'User:Bar',
unprefixed: 'Bar',
pages: [
'User:Bar',
'User talk:Bar'
],
count: 5
}
],
totalCount: 15,
source: {
title: 'Hebrew Wikipedia',
url: 'http://he.wiki.local.wmftest.net:8080/w/api.php',
base: 'http://he.wiki.local.wmftest.net:8080/wiki/$1'
}
}
};
QUnit.module( 'ext.echo.dm - mw.echo.dm.SourcePagesModel' );
QUnit.test( 'Creating source-page map', function ( assert ) {
var model = new mw.echo.dm.SourcePagesModel();
model.setAllSources( sources );
assert.equal(
model.getCurrentSource(),
'local',
'Default source is local'
);
assert.equal(
model.getCurrentPage(),
null,
'Default page is null'
);
assert.deepEqual(
model.getSourcesArray(),
[ 'local', 'hewiki' ],
'Source array includes all sources'
);
assert.equal(
model.getSourceTitle( 'hewiki' ),
'Hebrew Wikipedia',
'Source title'
);
assert.equal(
model.getSourceTotalCount( 'hewiki' ),
15,
'Source total count'
);
assert.deepEqual(
model.getSourcePages( 'local' ),
{
Moai: {
count: 3,
ns: 0,
pages: [
'Moai',
'Talk:Moai'
],
title: 'Moai',
unprefixed: 'Moai'
},
'User:Admin': {
count: 24,
ns: 2,
pages: [
'User:Admin',
'User talk:Admin',
null
],
title: 'User:Admin',
unprefixed: 'Admin'
},
'User:RandomUser': {
count: 6,
ns: 2,
pages: [
'User:RandomUser',
'User talk:RandomUser'
],
title: 'User:RandomUser',
unprefixed: 'RandomUser'
}
},
'Outputting source pages'
);
assert.deepEqual(
model.getGroupedPagesForTitle( 'local', 'User:Admin' ),
[
'User:Admin',
'User talk:Admin',
null
],
'Grouped pages per title'
);
// Change source
model.setCurrentSourcePage( 'hewiki', 'User:Bar' );
assert.equal(
model.getCurrentSource(),
'hewiki',
'Source changed successfully'
);
assert.equal(
model.getCurrentPage(),
'User:Bar',
'Page changed successfully'
);
} );
} )( mediaWiki );

View file

@ -1,256 +0,0 @@
( function ( mw, $ ) {
var echoApi,
mockCounter,
noop = function () {};
QUnit.module( 'ext.echo.dm mw.echo.dm.NotificationsModel' );
function runPreparation( model, testPrepare ) {
var j, jlen;
for ( j = 0, jlen = testPrepare.length; j < jlen; j++ ) {
model[ testPrepare[ j ].method ].apply( model, testPrepare[ j ].params );
}
}
// Helper method to get an array of item ids for testing
function getIdArray( arr ) {
return arr.map( function ( item ) {
return item.getId();
} );
}
// Set up a dummy API handler to avoid sending requests to the API during tests
function TestApiHandler() {
// Parent constructor
TestApiHandler.parent.call( this );
}
/* Setup */
OO.inheritClass( TestApiHandler, mw.echo.api.APIHandler );
// Override api call
TestApiHandler.prototype.markItemsRead = function () {
return $.Deferred().resolve( 0 );
};
mockCounter = { estimateChange: noop, update: noop };
// Create an Echo API instance
echoApi = new mw.echo.api.EchoApi();
// HACK: Reach into the EchoAPI to create a test handler
echoApi.network.setApiHandler( 'test', new TestApiHandler() );
QUnit.test( 'Adding notifications', function ( assert ) {
var initialItems = [
new mw.echo.dm.NotificationItem( 0, { timestamp: '20150828173000', read: false } ),
new mw.echo.dm.NotificationItem( 1, { timestamp: '20150828173100', read: false } ),
new mw.echo.dm.NotificationItem( 2, { timestamp: '20150828173200', read: false } )
],
cases = [
{
items: initialItems,
expected: [ 2, 1, 0 ],
msg: 'Inserting items in timestamp order.'
},
{
items: [
initialItems[ 0 ],
initialItems[ 1 ],
initialItems[ 2 ],
initialItems[ 0 ]
],
expected: [ 2, 1, 0 ],
msg: 'Reinserting an item to its rightful position.'
},
{
items: [
new mw.echo.dm.NotificationItem( 0, { timestamp: '20150828173000', read: false } ),
new mw.echo.dm.NotificationItem( 1, { timestamp: '20150828173100', read: false } ),
new mw.echo.dm.NotificationItem( 2, { timestamp: '20150828173200', read: false } )
],
run: [
{
item: 0,
method: 'setTimestamp',
args: [ '20150830173000' ] // Newer timestamp
}
],
expected: [ 0, 2, 1 ],
msg: 'Changing timestamp on an item.'
},
{
items: [
new mw.echo.dm.NotificationItem( 0, { timestamp: '20150828173000', read: false } ),
new mw.echo.dm.NotificationItem( 1, { timestamp: '20150828173100', read: false } ),
new mw.echo.dm.NotificationItem( 2, { timestamp: '20150828173200', read: false } )
],
run: [
{
item: 1,
method: 'toggleRead',
args: [ true ] // Item is read
}
],
expected: [ 2, 0, 1 ],
msg: 'Changing read status of an item.'
}
];
QUnit.expect( cases.length );
cases.forEach( function ( test ) {
var r, runCase, runItem,
model = new mw.echo.dm.NotificationsModel( echoApi, mockCounter, {
type: 'alert',
source: 'test',
limit: 25,
userLang: 'en'
} );
model.addItems( test.items );
if ( test.add ) {
model.addItems( test.add.items );
}
if ( test.run ) {
for ( r = 0; r < test.run.length; r++ ) {
runCase = test.run[ r ];
runItem = test.items[ runCase.item ];
runItem[ runCase.method ].apply( runItem, runCase.args );
}
}
assert.deepEqual( getIdArray( model.getItems() ), test.expected, test.msg );
}, this );
} );
QUnit.test( 'Deleting notifications', 2, function ( assert ) {
var model = new mw.echo.dm.NotificationsModel( echoApi, mockCounter, {
type: 'alert',
source: 'test',
limit: 25,
userLang: 'en'
} ),
items = [
new mw.echo.dm.NotificationItem( 1, { content: '1', timestamp: '20150828172900' } ),
new mw.echo.dm.NotificationItem( 2, { content: '2', timestamp: '20150828172900' } ),
new mw.echo.dm.NotificationItem( 3, { content: '3', timestamp: '20150828172900' } ),
new mw.echo.dm.NotificationItem( 4, { content: '4', timestamp: '20150828172900' } ),
new mw.echo.dm.NotificationItem( 5, { content: '5', timestamp: '20150828172900' } ),
new mw.echo.dm.NotificationItem( 6, { content: '6', timestamp: '20150828172900' } ),
new mw.echo.dm.NotificationItem( 7, { content: '7', timestamp: '20150828172900' } ),
new mw.echo.dm.NotificationItem( 8, { content: '8', timestamp: '20150828172900' } ),
new mw.echo.dm.NotificationItem( 9, { content: '9', timestamp: '20150828172900' } ),
new mw.echo.dm.NotificationItem( 10, { content: '10', timestamp: '20150828172900' } )
];
// Add initial notifications
model.addItems( items );
// Verify we have the correct number initially
assert.equal( model.getItemCount(), 10, 'Added initial number of notifications' );
// Remove notifications
model.removeItems( [ items[ 0 ], items[ 1 ], items[ 5 ] ] );
// Test
assert.equal( model.getItemCount(), 7, 'Successfully deleted notifications' );
} );
QUnit.test( 'Clearing notifications', function ( assert ) {
var i, ilen, model, actual, test,
cases = [
{
prepare: [
{
method: 'addItems',
params: [
[
new mw.echo.dm.NotificationItem( 1, {
content: '1',
timestamp: '20150828172900'
} ),
new mw.echo.dm.NotificationItem( 2, { content: '2', timestamp: '20150828172900' } )
]
]
},
{
method: 'clearItems'
}
],
run: {
method: 'getItemCount'
},
expect: 0,
message: 'Clearing notifications'
}
];
assert.expect( cases.length );
for ( i = 0, ilen = cases.length; i < ilen; i++ ) {
model = new mw.echo.dm.NotificationsModel( echoApi, mockCounter, {
type: 'alert',
source: 'test',
limit: 25,
userLang: 'en'
} );
test = cases[ i ];
// Run preparation
runPreparation( model, test.prepare );
// Test
actual = model[ test.run.method ].apply( model, cases[ i ].run.params );
assert.equal( actual, cases[ i ].expect, cases[ i ].message );
}
} );
QUnit.test( 'Changing read/unread status', function ( assert ) {
var i,
initialItems = [
new mw.echo.dm.NotificationItem( 0, { timestamp: '20150828173000', read: false } ),
new mw.echo.dm.NotificationItem( 1, { timestamp: '20150828173100', read: false } ),
new mw.echo.dm.NotificationItem( 2, { timestamp: '20150828173200', read: false } ),
new mw.echo.dm.NotificationItem( 3, { timestamp: '20150828173300', read: false } ),
// Notice that in item 4 the timestamp is earlier
new mw.echo.dm.NotificationItem( 4, { timestamp: '20150828172900', read: true } ),
new mw.echo.dm.NotificationItem( 5, { timestamp: '20150828173500', read: true } )
],
cases = [
{
items: initialItems,
expected: [ 3, 2, 1, 0, 5, 4 ],
msg: 'Items organized by read/unread groups'
},
{
items: initialItems,
markRead: [ initialItems[ 1 ], initialItems[ 3 ] ],
expected: [ 2, 0, 5, 3, 1, 4 ],
msg: 'Items marked as read are pushed to the end'
}
];
QUnit.expect( cases.length );
cases.forEach( function ( test ) {
var model = new mw.echo.dm.NotificationsModel( echoApi, mockCounter, {
type: 'alert',
source: 'test',
limit: 25,
userLang: 'en'
} );
model.addItems( test.items );
if ( test.markRead ) {
for ( i = 0; i < test.markRead.length; i++ ) {
test.markRead[ i ].toggleRead( true );
}
}
assert.deepEqual( getIdArray( model.getItems() ), test.expected, test.msg );
}, this );
} );
} )( mediaWiki, jQuery );