UNPKG

can

Version:

MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.

750 lines (590 loc) 20.1 kB
steal("can/list/sort", "can/test", "can/view/mustache", "can/view/stache", "can/model", "steal-qunit", function () { QUnit.module('can/list/sort'); test('List events', (4*3), function () { var list = new can.List([{ name: 'Justin' }, { name: 'Brian' }, { name: 'Austin' }, { name: 'Mihael' }]); list.attr('comparator','name'); // events on a list // - move - item from one position to another // due to changes in elements that change the sort order // - add (items added to a list) // - remove (items removed from a list) // - reset (all items removed from the list) // - change something happened // a move directly on this list list.bind('move', function (ev, item, newPos, oldPos) { ok(ev, '"move" event passed `ev`'); equal(item.name, 'Zed', '"move" event passed correct `item`'); equal(newPos, 3, '"move" event passed correct `newPos`'); equal(oldPos, 0, '"move" event passed correct `oldPos`'); }); // a remove directly on this list list.bind('remove', function (ev, items, oldPos) { ok(ev, '"remove" event passed ev'); equal(items.length, 1, '"remove" event passed correct # of `item`\'s'); equal(items[0].name, 'Alexis', '"remove" event passed correct `item`'); equal(oldPos, 0, '"remove" event passed correct `oldPos`'); }); list.bind('add', function (ev, items, index) { ok(ev, '"add" event passed ev'); equal(items.length, 1, '"add" event passed correct # of items'); equal(items[0].name, 'Alexis', '"add" event passed correct `item`'); equal(index, 0, '"add" event passed correct `index`'); }); // Push: Should result in a "add" event list.push({ name: 'Alexis' }); // Splice: Should result in a "remove" event list.splice(0, 1); // Update: Should result in a "move" event list[0].attr('name', 'Zed'); }); test('Passing a comparator function to sort()', 1, function () { var list = new can.List([{ priority: 4, name: 'low' }, { priority: 1, name: 'high' }, { priority: 2, name: 'middle' }, { priority: 3, name: 'mid' }]); list.sort(function (a, b) { // Sort functions always need to return the -1/0/1 integers if (a.priority < b.priority) { return -1; } return a.priority > b.priority ? 1 : 0; }); equal(list[0].name, 'high'); }); test('Passing a comparator string to sort()', 1, function () { var list = new can.List([{ priority: 4, name: 'low' }, { priority: 1, name: 'high' }, { priority: 2, name: 'middle' }, { priority: 3, name: 'mid' }]); list.sort('priority'); equal(list[0].name, 'high'); }); test('Defining a comparator property', 1, function () { var list = new can.List([{ priority: 4, name: 'low' }, { priority: 1, name: 'high' }, { priority: 2, name: 'middle' }, { priority: 3, name: 'mid' }]); list.attr('comparator','priority'); equal(list[0].name, 'high'); }); test('Defining a comparator property that is a function of a can.Map', 4, function () { var list = new can.Map.List([ new can.Map({ text: 'Bbb', func: can.compute(function () { return 'bbb'; }) }), new can.Map({ text: 'abb', func: can.compute(function () { return 'abb'; }) }), new can.Map({ text: 'Aaa', func: can.compute(function () { return 'aaa'; }) }), new can.Map({ text: 'baa', func: can.compute(function () { return 'baa'; }) }) ]); list.attr('comparator','func'); equal(list.attr()[0].text, 'Aaa'); equal(list.attr()[1].text, 'abb'); equal(list.attr()[2].text, 'baa'); equal(list.attr()[3].text, 'Bbb'); }); test('Sorts primitive items', function () { var list = new can.List(['z', 'y', 'x']); list.sort(); equal(list[0], 'x', 'Moved string to correct index'); }); function renderedTests (templateEngine, helperType, renderer) { test('Insert pushed item at correct index with ' + templateEngine + ' using ' + helperType +' helper', function () { var el = document.createElement('div'); var items = new can.List([{ id: 'b' }]); items.attr('comparator', 'id'); // Render the template and place inside the <div> el.appendChild(renderer({ items: items })); var firstElText = el.querySelector('li').firstChild.data; /// Check that the template rendered an item equal(firstElText, 'b', 'First LI is a "b"'); // Add another item items.push({ id: 'a' }); // Get the text of the first <li> in the <div> firstElText = el.querySelector('li').firstChild.data; // Check that the template rendered that item at the correct index equal(firstElText, 'a', 'An item pushed into the list is rendered at the correct position'); }); // TODO: Test that push and sort have the result in the same output test('Insert unshifted item at correct index with ' + templateEngine + ' using ' + helperType +' helper', function () { var el = document.createElement('div'); var items = new can.List([ { id: 'a' }, { id: 'c' } ]); items.attr('comparator', 'id'); // Render the template and place inside the <div> el.appendChild(renderer({ items: items })); var firstElText = el.querySelector('li').firstChild.data; /// Check that the template rendered an item equal(firstElText, 'a', 'First LI is a "a"'); // Attempt to add an item to the beginning of the list items.unshift({ id: 'b' }); firstElText = el.querySelectorAll('li')[1].firstChild.data; // Check that the template rendered that item at the correct index equal(firstElText, 'b', 'An item unshifted into the list is rendered at the correct position'); }); test('Insert spliced item at correct index with ' + templateEngine + ' using ' + helperType +' helper', function () { var el = document.createElement('div'); var items = new can.List([ { id: 'b' }, { id: 'c' } ]); items.attr('comparator','id'); // Render the template and place inside the <div> el.appendChild(renderer({ items: items })); var firstElText = el.querySelector('li').firstChild.data; // Check that the "b" is at the beginning of the list equal(firstElText, 'b', 'First LI is a b'); // Add a "1" to the middle of the list items.splice(1, 0, { id: 'a' }); // Get the text of the first <li> in the <div> firstElText = el.querySelector('li').firstChild.data; // Check that the "a" was added to the beginning of the list despite // the splice equal(firstElText, 'a', 'An item spliced into the list at the wrong position is rendered ' + 'at the correct position'); }); // TODO: Test adding and removing items at the same time with .splice() test('Moves rendered item to correct index after "set" using ' + helperType +' helper', function () { var el = document.createElement('div'); var items = new can.List([ { id: 'x' }, { id: 'y' }, { id: 'z' } ]); items.attr('comparator', 'id'); // Render the template and place inside the <div> el.appendChild(renderer({ items: items })); var firstElText = el.querySelector('li').firstChild.data; // Check that the "x" is at the beginning of the list equal(firstElText, 'x', 'First LI is a "x"'); // Change the ID of the last item so that it's sorted above the first item items.attr('2').attr('id', 'a'); // Get the text of the first <li> in the <div> firstElText = el.querySelector('li').firstChild.data; // Check that the "a" was added to the beginning of the list despite // the splice equal(firstElText, 'a', 'The last item was moved to the first position ' + 'after it\'s value was changed'); }); test('Move DOM items when list is sorted with ' + templateEngine + ' using the ' + helperType +' helper', function () { var el = document.createElement('div'); var items = new can.List([ { id: 4 }, { id: 1 }, { id: 6 }, { id: 3 }, { id: 2 }, { id: 8 }, { id: 0 }, { id: 5 }, { id: 6 }, { id: 9 }, ]); // Render the template and place inside the <div> el.appendChild(renderer({ items: items })); var firstElText = el.querySelector('li').firstChild.data; // Check that the "4" is at the beginning of the list equal(firstElText, 4, 'First LI is a "4"'); // Sort the list in-place items.attr('comparator' , 'id'); firstElText = el.querySelector('li').firstChild.data; equal(firstElText, 0, 'The `0` was moved to beginning of the list' + 'once sorted.'); }); test('Push multiple items with ' + templateEngine + ' using the ' + helperType +' helper (#1509)', function () { var el = document.createElement('div'); var items = new can.List(); items.attr('comparator' , 'id'); // Render the template and place inside the <div> el.appendChild(renderer({ items: items })); items.bind('add', function (ev, items) { equal(items.length, 1, 'One single item was added'); }); items.push.apply(items, [ { id: 4 }, { id: 1 }, { id: 6 } ]); var liLength = el.getElementsByTagName('li').length; equal(liLength, 3, 'The correct number of items have been rendered'); }); } var blockHelperTemplate = '<ul>{{#items}}<li>{{id}}</li>{{/items}}'; var eachHelperTemplate = '<ul>{{#each items}}<li>{{id}}</li>{{/each}}'; renderedTests('Mustache', '{{#block}}', can.mustache(blockHelperTemplate)); renderedTests('Stache', '{{#block}}', can.stache(blockHelperTemplate)); renderedTests('Mustache', '{{#each}}', can.mustache(eachHelperTemplate)); renderedTests('Stache', '{{#each}}', can.stache(eachHelperTemplate)); test('Sort primitive values without a comparator defined', function () { var list = new can.List([8,5,2,1,5,9,3,5]); list.sort(); equal(list[0], 1, 'Sorted the list in ascending order'); }); test('Sort primitive values with a comparator function defined', function () { var list = new can.List([8,5,2,1,5,9,3,5]); list.attr('comparator' , function (a, b) { return a === b ? 0 : a < b ? 1 : -1; }); equal(list[0], 9, 'Sorted the list in descending order'); }); test('The "destroyed" event bubbles on a sorted list', 2, function () { var list = new can.Model.List([ new can.Model({ name: 'Joe' }), new can.Model({ name: 'Max' }), new can.Model({ name: 'Pim' }) ]); list.attr('comparator' , 'name'); list.bind('destroyed', function (ev) { ok(true, '"destroyed" event triggered'); }); list.attr(0).destroy(); equal(list.attr('length'), 2, 'item removed'); }); test("sorting works with #each (#1566)", function(){ var heroes = new can.List([ { id: 1, name: 'Superman'}, { id: 2, name: 'Batman'} ]); heroes.attr('comparator', 'name'); var template = can.stache("<ul>\n{{#each heroes}}\n<li>{{id}}-{{name}}</li>\n{{/each}}</ul>"); var frag = template({ heroes: heroes }); var lis = frag.childNodes[0].getElementsByTagName("li"); equal(lis[0].innerHTML, "2-Batman"); equal(lis[1].innerHTML, "1-Superman"); heroes.attr('comparator', 'id'); equal(lis[0].innerHTML, "1-Superman"); equal(lis[1].innerHTML, "2-Batman"); }); test("sorting works with comparator added after a binding", function(){ var heroes = new can.List([ { id: 1, name: 'Superman'}, { id: 2, name: 'Batman'} ]); var template = can.stache("<ul>\n{{#each heroes}}\n<li>{{id}}-{{name}}</li>\n{{/each}}</ul>"); var frag = template({ heroes: heroes }); heroes.attr('comparator', 'id'); heroes.attr("0.id",3); var lis = frag.childNodes[0].getElementsByTagName("li"); equal(lis[0].innerHTML, "2-Batman"); equal(lis[1].innerHTML, "3-Superman"); }); test("removing comparator tears down bubbling", function(){ var heroes = new can.List([ { id: 1, name: 'Superman'}, { id: 2, name: 'Batman'} ]); var lengthHandler = function(){}; heroes.bind("length",lengthHandler); ok(!heroes[0]._bindings, "item has no bindings"); heroes.attr('comparator', 'id'); heroes.attr("0.id",3); ok(heroes._bindings, "list has bindings"); ok(heroes[0]._bindings, "item has bindings"); heroes.removeAttr('comparator'); ok(!heroes[0]._bindings, "has bindings"); ok(heroes._bindings, "list has bindings"); heroes.unbind("length",lengthHandler); ok(!heroes._bindings, "list has no bindings"); }); test('sorting works when returning any negative value (#1601)', function() { var list = new can.List([1, 4, 2]); list.attr('comparator', function(a, b) { return a - b; }); list.sort(); deepEqual(list.attr(), [1, 2, 4]); }); test('Batched events originating from sort plugin lack batchNum (#1707)', function () { var list = new can.List(); list.attr('comparator', 'id'); list.bind('length', function (ev) { ok(ev.batchNum, 'Has batchNum'); }); can.batch.start(); list.push({ id: 'a' }); list.push({ id: 'a' }); list.push({ id: 'a' }); can.batch.stop(); }); test('The sort plugin\'s _change handler ignores batched _changes (#1706)', function () { var list = new can.List(); var _getRelativeInsertIndex = list._getRelativeInsertIndex; var sort = list.sort; list.attr('comparator', 'id'); list.bind('move', function (ev) { ok(false, 'No "move" events should be fired'); }); list._getRelativeInsertIndex = function () { ok(false, 'No items should be evaluated independently'); return _getRelativeInsertIndex.apply(this, arguments); }; list.sort = function () { ok(true, 'Batching caused sort() to be called'); return sort.apply(this, arguments); }; can.batch.start(); list.push({ id: 'c', index: 1 }); list.push({ id: 'a', index: 2 }); list.push({ id: 'a', index: 3 }); can.batch.stop(); equal(list.attr('2.id'), 'c', 'List was sorted'); }); test('Items aren\'t unecessarily swapped to the end of a list of equal items (#1705)', function () { var list = new can.List([ { id: 'a', index: 1 }, { id: 'b', index: 2 }, { id: 'c', index: 3 } ]); list.attr('comparator', 'id'); list.bind('move', function () { ok(false, 'No "move" events should be fired'); }); list.attr('0.id', 'b'); equal(list.attr('0.index'), 1, 'Item hasn\'t moved'); ok(true, '_getRelativeInsertIndex prevented an unecessary \'move\' event'); }); test('Items aren\'t unecessarily swapped to the beginning of a list of equal items (#1705)', function () { var list = new can.List([ { id: 'a', index: 1 }, { id: 'b', index: 2 }, { id: 'c', index: 3 } ]); list.attr('comparator', 'id'); list.bind('move', function () { ok(false, 'No "move" events should be fired'); }); list.attr('2.id', 'b'); equal(list.attr('2.index'), 3, 'Item hasn\'t moved'); ok(true, '_getRelativeInsertIndex prevented an unecessary \'move\' event'); }); test('Insert index is not evaluted for irrelevant changes', function () { var list = new can.List([ { id: 'a', index: 1 }, { id: 'b', index: 2, child: { grandchild: { id: 'c', index: 3 } } } ]); // Setup var _getRelativeInsertIndex = list._getRelativeInsertIndex; list.bind('move', function (ev) { ok(false, 'A "move" events should be fired'); }); list._getRelativeInsertIndex = function () { ok(false, 'This item should not be evaluated independently'); return _getRelativeInsertIndex.apply(this, arguments); }; list.attr('comparator', 'id'); // Start test list.attr('0.index', 4); list.attr('comparator', 'child.grandchild.id'); list.attr('1.child.grandchild.index', 4); list._getRelativeInsertIndex = function () { ok(true, 'This item should be evaluated independently'); return _getRelativeInsertIndex.apply(this, arguments); }; list.attr('1.child', { grandchild: { id: 'c', index: 4 } }); equal(list.attr('0.id'), 'a', 'Item not moved'); }); test('_getInsertIndex positions items correctly', function () { var letters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; var alphabet = letters.split(''); var expected = alphabet.slice(0); var sorted = new can.List(alphabet); // Enable the sort plugin sorted.attr('comparator', can.List.prototype._comparator); // There are some gotcha's that we can't compare to native sort: // http://blog.rodneyrehm.de/archives/14-Sorting-Were-Doing-It-Wrong.html var samples = ['0A','ZZ','**','LM','LL','Josh','James','Juan','Julia', '!!HOORAY!!']; can.each(samples, function (value) { expected.push(value); expected.sort(can.List.prototype._comparator); sorted.push(value); can.each(expected, function (value, index) { equal(value, sorted.attr(index), 'Sort plugin output matches native output'); }); }); }); test('set comparator on init', function() { var Item = can.Map.extend(); Item.List = Item.List.extend({ init: function() { this.attr('comparator', 'isPrimary'); } }); var items = [ { isPrimary: false }, { isPrimary: true }, { isPrimary: false } ]; deepEqual(new Item.List(items).serialize(), [ { isPrimary: false }, { isPrimary: false }, { isPrimary: true } ]); }); test('{{@index}} is updated for "move" events (#1962)', function () { var list = new can.List([100, 200, 300]); list.attr('comparator', function (a, b) { return a < b ? -1 : 1; }); var template = can.stache('<ul>{{#each list}}<li>' + '<span class="index">{{@index}}</span> - ' + '<span class="value">{{.}}</span>' + '</li>{{/each}}</ul>'); var frag = template({ list: list }); var expected; var evaluate = function () { var liEls = frag.querySelectorAll('li'); for (var i = 0; i < expected.length; i++) { var li = liEls[i]; var index = li.querySelectorAll('.index')[0].innerHTML; var value = li.querySelectorAll('.value')[0].innerHTML; equal(index, ''+i, '{{@index}} rendered correct value'); equal(value, ''+expected[i], '{{.}} rendered correct value'); } }; expected = [100, 200, 300]; evaluate(); list.attr('comparator', function (a, b) { return a < b ? 1 : -1; }); expected = [300, 200, 100]; evaluate(); }); test(".sort(comparatorFn) is passed list items regardless of .attr('comparator') value (#2159)", function () { var list = new can.List([ { letter: 'x', number: 3 }, { letter: 'y', number: 2 }, { letter: 'z', number: 1 }, ]); list.attr('comparator', 'number'); equal(list.attr('0.number'), 1, 'First value is correct'); equal(list.attr('1.number'), 2, 'Second value is correct'); equal(list.attr('2.number'), 3, 'Third value is correct'); list.sort(function (a, b) { a = a.attr('letter'); b = b.attr('letter'); return (a === b) ? 0 : (a < b) ? -1 : 1; }); equal(list.attr('0.letter'), 'x', 'First value is correct after sort with single use comparator'); equal(list.attr('1.letter'), 'y', 'Second value is correct after sort with single use comparator'); equal(list.attr('2.letter'), 'z', 'Third value is correct after sort with single use comparator'); }); test("List is not sorted on change after calling .sort(fn)", function () { var list = new can.List([ { letter: 'x', number: 3 }, { letter: 'y', number: 2 }, { letter: 'z', number: 1 }, ]); list.sort(function (a, b) { a = a.attr('letter'); b = b.attr('letter'); return (a === b) ? 0 : (a < b) ? -1 : 1; }); equal(list.attr('0.letter'), 'x', 'First value is correct after sort with single use comparator'); equal(list.attr('1.letter'), 'y', 'Second value is correct after sort with single use comparator'); equal(list.attr('2.letter'), 'z', 'Third value is correct after sort with single use comparator'); list.sort = function () { ok(false, 'The list is not sorted as a result of change'); }; list.attr('2.letter', 'a'); equal(list.attr('0.letter'), 'x','First value is still correct'); equal(list.attr('1.letter'), 'y', 'Second value is still correct'); equal(list.attr('2.letter'), 'a', 'Third value is correctly out of place'); }); });