define([ 'intern!tdd', 'intern/chai!assert', 'dojo/_base/declare', 'dojo/aspect', 'dojo/dom-class', 'dojo/query', '../../data/createSyncStore', 'dgrid/OnDemandList' ], function (test, assert, declare, aspect, domClass, query, createSyncStore, OnDemandList) { var widget, storeCounter = 0; function destroyWidget() { if (widget) { widget.destroy(); widget = null; } } function indexToId(index) { return (index + 1) * 10; } function createItem(index) { var id = indexToId(index); return { id: id, value: 'Value ' + id + ' / Store ' + storeCounter }; } function createData(numStoreItems) { var data = []; for (var i = 0; i < numStoreItems; i++) { data.push(createItem(i)); } return data; } function createStore(numStoreItems) { storeCounter++; return createSyncStore({ data: createData(numStoreItems) }); } function renderRow(object) { var div = document.createElement('div'); div.appendChild(document.createTextNode(object.value)); return div; } function createList(numStoreItems, itemsPerQuery, overlap, shouldTrackCollection) { widget = new OnDemandList({ collection: createStore(numStoreItems), minRowsPerPage: itemsPerQuery, maxRowsPerPage: itemsPerQuery, queryRowsOverlap: overlap, renderRow: renderRow, shouldTrackCollection: shouldTrackCollection !== false, sort: 'id' }); document.body.appendChild(widget.domNode); widget.startup(); } function itemTest(itemAction, index, numToModify, backwards) { // Creates a single test case for performing an action on numToModify rows/items. var description = itemAction.actionName + ' ' + numToModify + ' item' + (numToModify > 1 ? 's' : '') + ' starting at index ' + index + ', in ' + (backwards ? 'decreasing' : 'increasing') + ' order'; numToModify = numToModify || 1; test.test(description, function () { var i, cnt, step = function () { cnt++; backwards ? i-- : i++; }, tmp, expectedValues = [], msgPrefix; function testRow(element, i) { var expectedValue = expectedValues[i]; if (expectedValue == null || expectedValue.deleted) { assert.isTrue(element == null, msgPrefix + 'row at index ' + i + ' should not be found'); } else { expectedValue = expectedValue.value; assert.isTrue(element != null, msgPrefix + 'row at index ' + i + ' with an expected value of "' + expectedValue + '" is missing'); assert.strictEqual(expectedValue, element.innerHTML, msgPrefix + element.innerHTML + ' should be ' + expectedValue); } } // Perform the actions and update the array of expected values. expectedValues = createData(widget.collection.data.length); for (i = index, cnt = 0; cnt < numToModify; step()) { itemAction(indexToId(i), expectedValues); } // Use the dgrid widget API to test if the action was performed properly. msgPrefix = 'dgrid API: '; tmp = []; for (i = 0; i < expectedValues.length; i++) { var expectedValue = expectedValues[i], expectedId = expectedValue.id; testRow(widget.row(expectedId).element, i); if (!expectedValue.deleted) { tmp.push(expectedValue); } } expectedValues = tmp; // Query the DOM to verify the structure matches the expected results. msgPrefix = 'DOM query: '; query('.dgrid-row', widget.domNode).forEach(testRow); }); } function itemTestSuite(widgetClassName, storeSize, itemsPerQuery, overlap, config) { // Create a test suite that performs one action type (itemAction) on 1 to config.itemsModifiedMax with // a given amount of overlap. var index, numToModify; test.suite(widgetClassName + ' with ' + overlap + ' overlap', function () { test.beforeEach(function () { createList(storeSize, itemsPerQuery, overlap); }); test.afterEach(destroyWidget); // Modify items counting up. for (numToModify = 1; numToModify <= config.itemsModifiedMax; numToModify++) { for (index = 0; index <= (storeSize - numToModify); index++) { itemTest(config.itemAction, index, numToModify); } } // Modify items counting down. Starting at a count of 2 because // single item modification were tested above. for (numToModify = 2; numToModify <= config.itemsModifiedMax; numToModify++) { for (index = numToModify - 1; index < storeSize; index++) { itemTest(config.itemAction, index, numToModify, true); } } }); } function itemActionTestSuite(description, itemAction, config) { // Creates multiple item test suites for a given action (itemAction): // - a list that executes a single query // - lists with overlap from 0 to config.itemOverlapMax // Note: for debugging, comment out the contents of destroyWidget so the dgrid widgets are not destroyed. // Each widget uses a different store id and those ids are used in the row contents allowing you to // easily match up an error message like // "Error: dgrid API: row at index 2 with an expected value of "Value 30 / Store 10 / Changed!" is missing" // with the correct widget on the page. config.itemAction = itemAction; test.suite(description, function () { // Test widgets with only one query: total item count equals item count per query. itemTestSuite('OnDemandList one query', config.itemsPerQuery, config.itemsPerQuery, 0, config); // Test widgets that make multiple query requests: twice as many items as items per query so multiple // queries will create multiple observers. var storeSize = config.itemsPerQuery * 2; // Test with OnDemandList with varying overlap values for (var overlap = 0; overlap <= config.itemOverlapMax; overlap++) { itemTestSuite('OnDemandList multiple queries', storeSize, config.itemsPerQuery, overlap, config); } }); } function itemAddEmptyStoreTest(itemsToAddCount, itemsPerQuery, overlap) { var i; function rowHasClass(rowNode, cssClass) { assert.isTrue(domClass.contains(rowNode, cssClass), rowNode.outerHTML + ' should have ' + cssClass); } test.test('Add ' + itemsToAddCount + ' items with ' + overlap + ' overlap', function () { createList(0, itemsPerQuery, overlap); var store = widget.collection; for (i = 0; i < itemsToAddCount; i++) { store.put(createItem(i)); } var rows = query('.dgrid-content > div', widget.domNode); rowHasClass(rows[0], 'dgrid-preload'); for (i = 1; i <= itemsToAddCount; i++) { rowHasClass(rows[i], (i % 2) ? 'dgrid-row-even' : 'dgrid-row-odd'); } rowHasClass(rows[i], 'dgrid-preload'); for (i = 0; i < itemsToAddCount; i++) { store.put(createItem(i)); } rows = query('.dgrid-content > div', widget.domNode); rowHasClass(rows[0], 'dgrid-preload'); for (i = 1; i <= itemsToAddCount; i++) { rowHasClass(rows[i], (i % 2) ? 'dgrid-row-even' : 'dgrid-row-odd'); } rowHasClass(rows[i], 'dgrid-preload'); }); } function itemAddEmptyStoreTestSuite(config) { test.suite('Add items to empty store', function () { test.afterEach(destroyWidget); itemAddEmptyStoreTest(1, config.itemsPerQuery, 0); // Test with OnDemandList with varying overlap values for (var overlap = 0; overlap <= config.itemOverlapMax; overlap++) { itemAddEmptyStoreTest(config.itemsPerQuery + overlap + 1, config.itemsPerQuery, overlap); } }); } test.suite('Trackable lists', function () { // Creates test suites that execute the following actions on OnDemandLists with varying amount of // overlap and modifying varying number of items: // - modify existing items // - remove existing items // - add new items before existing items // - add new items after existing items function findIndex(id, objs) { for (var i = 0; i < objs.length; i++) { var obj = objs[i]; if (obj && obj.id === id) { return i; } } return -1; } var modifyAction = function (id, expectedValues) { var index = findIndex(id, expectedValues); var value = expectedValues[index].value + ' / Changed!'; var dataObj = {id: id, value: value}; widget.collection.put(dataObj); expectedValues[index] = dataObj; }; modifyAction.actionName = 'Modify'; var removeAction = function (id, expectedValues) { widget.collection.remove(id); var index = findIndex(id, expectedValues); expectedValues[index].deleted = true; }; removeAction.actionName = 'Remove'; var addBeforeAction = function (id, expectedValues) { var index = findIndex(id, expectedValues); var obj = { id: id - 5, value: expectedValues[index].value + ' / Added before!' }; widget.collection.add(obj); expectedValues.splice(index, 0, obj); }; addBeforeAction.actionName = 'Add before'; var addAfterAction = function (id, expectedValues) { var index = findIndex(id, expectedValues); var obj = {id: id + 5, value: expectedValues[index].value + ' / Added after!'}; widget.collection.add(obj); expectedValues.splice(index + 1, 0, obj); }; addAfterAction.actionName = 'Add after'; // Run a test case with each action (modify, remove, add before, add after) and vary the amount of // queryRowsOverlap and vary the number of items modified during each test case. A configuration // object controls the amount of variation. The properties are: // itemsPerQuery - The OnDemandList is configured to request this number of items per query. // This property also determines the size of the store. Test cases run with a store size // equal to this number and test cases run with a store size twice this number. // itemOverlapMax - Each test case is executed with a queryRowsOverap value of 0 up to this number. // itemsModifiedMax - Each test is executed where the number of items modified, deleted or added is // 1 up to this number; all of the test cases are run where 1 item is modified and then again // with 2 items being modified and so on. var config = { itemsPerQuery: 3, itemOverlapMax: 2, itemsModifiedMax: 2 }; itemActionTestSuite('Modify store items', modifyAction, config); itemActionTestSuite('Remove store items', removeAction, config); itemActionTestSuite('Insert store items before', addBeforeAction, config); itemActionTestSuite('Insert store items after', addAfterAction, config); itemAddEmptyStoreTestSuite(config); }); test.suite('Multiple updates to trackable list', function () { test.afterEach(destroyWidget); test.test('Multiple puts', function () { var i; var data = []; for (i = 0; i < 10; i++) { data.push({ id: i, value: 'Value ' + i, enabled: true }); } var store = createSyncStore({ data: data }); widget = new OnDemandList({ collection: store.filter({ enabled: true }), renderRow: renderRow }); document.body.appendChild(widget.domNode); widget.startup(); // Test putting several items with a filter applied; // the put objects should all be seen as removed for (i = 0; i < 5; i++){ var item = data[i]; item.enabled = false; store.put(item); } assert.strictEqual(widget._rows.length, 5, '5 items should remain in the _rows array (5 should be removed)'); }); }); test.suite('shouldTrackCollection = false + store modifications', function () { var numItems = 3; var store; var handles = []; test.before(function () { createList(numItems, 25, 0, false); }); test.beforeEach(function () { store = createStore(numItems); widget.set('collection', store); }); test.afterEach(function () { for (var i = handles.length; i--;) { handles[i].remove(); } handles = []; }); test.after(destroyWidget); function countRows() { var count = query('.dgrid-row', widget.contentNode).length; return count; } test.test('shouldTrackCollection = false + add', function () { var numRows = countRows(); store.addSync(createItem(3)); assert.strictEqual(countRows(), numRows); }); test.test('shouldTrackCollection = false + put', function () { var calls = 0; handles.push(aspect.before(widget, 'removeRow', function () { calls++; })); handles.push(aspect.before(widget, 'insertRow', function () { calls++; })); for (var i = 0; i < numItems; i++) { store.putSync(store.getSync(indexToId(i))); } assert.strictEqual(calls, 0, 'insertRow and removeRow should never be called'); }); test.test('shouldTrackCollection = false + remove', function () { var numRows = countRows(); for (var i = 0; i < numItems; i++) { store.removeSync(indexToId(i)); } assert.strictEqual(countRows(), numRows); }); }); });