UNPKG

angular-material-npfixed

Version:

The Angular Material project is an implementation of Material Design in Angular.js. This project provides a set of reusable, well-tested, and accessible Material Design UI components. Angular Material is supported internally at Google by the Angular.js, M

755 lines (607 loc) 24.5 kB
describe('<md-virtual-repeat>', function() { var MAX_ELEMENT_PIXELS = 10000; beforeEach(module('material.components.virtualRepeat', function($provide) { /* * Overwrite the $mdConstant ELEMENT_MAX_PIXELS property, because for testing it requires too much * memory and crashes the tests sometimes. */ $provide.decorator('$mdConstant', function($delegate) { $delegate.ELEMENT_MAX_PIXELS = MAX_ELEMENT_PIXELS; return $delegate; }) })); var VirtualRepeatController = { NUM_EXTRA : 3 }; var CONTAINER_HTML = '<md-virtual-repeat-container style="height:100px; width:150px">'+ '</md-virtual-repeat-container>'; var REPEATER_HTML = ''+ '<div md-virtual-repeat="i in items" ' + ' md-item-size="10" ' + ' md-start-index="startIndex" ' + ' style="height: 10px; width: 10px; box-sizing: border-box;">' + ' {{i}} {{$index}}' + '</div>'; var container, repeater, component, $$rAF, $compile, $document, $mdUtil, $window, scope, scroller, sizer, offsetter; var NUM_ITEMS = 110, VERTICAL_PX = 100, HORIZONTAL_PX = 150, ITEM_SIZE = 10; beforeEach(inject(function( _$$rAF_, _$compile_, _$document_, _$mdUtil_, $rootScope, _$window_, _$material_) { repeater = angular.element(REPEATER_HTML); container = angular.element(CONTAINER_HTML).append(repeater); component = null; $$rAF = _$$rAF_; $material = _$material_; $mdUtil = _$mdUtil_; $compile = _$compile_; $document = _$document_; $window = _$window_; scope = $rootScope.$new(); scope.startIndex = 0; scroller = null; sizer = null; offsetter = null; })); afterEach(function() { container.remove(); component && component.remove(); scope.$destroy(); }); function createRepeater() { angular.element($document[0].body).append(container); component = $compile(container)(scope); scroller = angular.element(component[0].querySelector('.md-virtual-repeat-scroller')); sizer = angular.element(component[0].querySelector('.md-virtual-repeat-sizer')); offsetter = angular.element(component[0].querySelector('.md-virtual-repeat-offsetter')); $material.flushOutstandingAnimations(); return component; } function createItems(num, label) { var items = []; for (var i = 0; i < num; i++) { items.push(label || 's' + (i * 2) + 's'); } return items; } function getRepeated() { return component[0].querySelectorAll('[md-virtual-repeat]'); } it('should $emit $md-resize-enable at startup', function() { var emitted = false; scope.$on('$md-resize-enable', function() { emitted = true; }); createRepeater(); expect(emitted).toBe(true); }); it('should render only enough items to fill the viewport + 3 (vertical)', function() { createRepeater(); scope.items = createItems(NUM_ITEMS); scope.$apply(); $$rAF.flush(); var numItemRenderers = VERTICAL_PX / ITEM_SIZE + VirtualRepeatController.NUM_EXTRA; expect(getRepeated().length).toBe(numItemRenderers); expect(sizer[0].offsetHeight).toBe(NUM_ITEMS * ITEM_SIZE); }); it('should render only enough items to fill the viewport + 3 (horizontal)', function() { container.attr('md-orient-horizontal', ''); createRepeater(); scope.items = createItems(NUM_ITEMS); scope.$apply(); $$rAF.flush(); var numItemRenderers = HORIZONTAL_PX / ITEM_SIZE + VirtualRepeatController.NUM_EXTRA; expect(getRepeated().length).toBe(numItemRenderers); expect(sizer[0].offsetWidth).toBe(NUM_ITEMS * ITEM_SIZE); }); it('should render only enough items to fill the viewport + 3 (vertical, no md-item-size)', function() { repeater.removeAttr('md-item-size'); createRepeater(); scope.items = createItems(NUM_ITEMS); $material.flushInterimElement(); var numItemRenderers = VERTICAL_PX / ITEM_SIZE + VirtualRepeatController.NUM_EXTRA; expect(getRepeated().length).toBe(numItemRenderers); expect(sizer[0].offsetHeight).toBe(NUM_ITEMS * ITEM_SIZE); }); it('should render only enough items to fill the viewport + 3 (horizontal, no md-item-size)', function() { container.attr('md-orient-horizontal', ''); repeater.removeAttr('md-item-size'); createRepeater(); scope.items = createItems(NUM_ITEMS); scope.$digest(); $$rAF.flush(); $$rAF.flush(); var numItemRenderers = HORIZONTAL_PX / ITEM_SIZE + VirtualRepeatController.NUM_EXTRA; expect(getRepeated().length).toBe(numItemRenderers); expect(sizer[0].offsetWidth).toBe(NUM_ITEMS * ITEM_SIZE); }); it('should reposition and swap items on scroll (vertical)', function() { createRepeater(); scope.items = createItems(NUM_ITEMS); scope.$apply(); $$rAF.flush(); var repeated; // Don't quite scroll past the first item. scroller[0].scrollTop = ITEM_SIZE - 1; scroller.triggerHandler('scroll'); expect(getTransform(offsetter)).toBe('translateY(0px)'); repeated = getRepeated(); expect(repeated.length).toBe(VERTICAL_PX / ITEM_SIZE + VirtualRepeatController.NUM_EXTRA); expect(repeated[0].textContent.trim()).toBe('s0s 0'); // Scroll past the first item. // Expect that one new item is created. scroller[0].scrollTop = ITEM_SIZE; scroller.triggerHandler('scroll'); expect(getTransform(offsetter)).toBe('translateY(0px)'); repeated = getRepeated(); expect(repeated.length).toBe(VERTICAL_PX / ITEM_SIZE + VirtualRepeatController.NUM_EXTRA + 1); expect(repeated[0].textContent.trim()).toBe('s0s 0'); // Scroll past the fourth item. // Expect that we now have the full set of extra items above and below. scroller[0].scrollTop = ITEM_SIZE * (VirtualRepeatController.NUM_EXTRA + 1); scroller.triggerHandler('scroll'); expect(getTransform(offsetter)).toBe('translateY(10px)'); repeated = getRepeated(); expect(repeated.length).toBe(VERTICAL_PX / ITEM_SIZE + VirtualRepeatController.NUM_EXTRA * 2); expect(repeated[0].textContent.trim()).toBe('s2s 1'); // Scroll to the end. // Expect the bottom extra items to be removed (pooled). scroller[0].scrollTop = 1000; scroller.triggerHandler('scroll'); expect(getTransform(offsetter)).toBe('translateY(970px)'); repeated = getRepeated(); expect(repeated.length).toBe(VERTICAL_PX / ITEM_SIZE + VirtualRepeatController.NUM_EXTRA); expect(repeated[0].textContent.trim()).toBe('s194s 97'); }); it('should reposition and swap items on scroll (horizontal)', function() { container.attr('md-orient-horizontal', ''); createRepeater(); scope.items = createItems(NUM_ITEMS); scope.$apply(); $$rAF.flush(); var repeated; // Don't quite scroll past the first item. scroller[0].scrollLeft = ITEM_SIZE - 1; scroller.triggerHandler('scroll'); expect(getTransform(offsetter)).toBe('translateX(0px)'); repeated = getRepeated(); expect(repeated.length).toBe(HORIZONTAL_PX / ITEM_SIZE + VirtualRepeatController.NUM_EXTRA); expect(repeated[0].textContent.trim()).toBe('s0s 0'); // Scroll past the first item. // Expect that we now have the full set of extra items above and below. scroller[0].scrollLeft = ITEM_SIZE; scroller.triggerHandler('scroll'); expect(getTransform(offsetter)).toBe('translateX(0px)'); repeated = getRepeated(); expect(repeated.length) .toBe(HORIZONTAL_PX / ITEM_SIZE + VirtualRepeatController.NUM_EXTRA + 1); expect(repeated[0].textContent.trim()).toBe('s0s 0'); // Scroll past the fourth item. // Expect that one new item is created. scroller[0].scrollLeft = ITEM_SIZE * (VirtualRepeatController.NUM_EXTRA + 1); scroller.triggerHandler('scroll'); expect(getTransform(offsetter)).toBe('translateX(10px)'); repeated = getRepeated(); expect(repeated.length) .toBe(HORIZONTAL_PX / ITEM_SIZE + VirtualRepeatController.NUM_EXTRA * 2); expect(repeated[0].textContent.trim()).toBe('s2s 1'); // Scroll to the end. // Expect the bottom extra items to be removed (pooled). scroller[0].scrollLeft = 950; scroller.triggerHandler('scroll'); expect(getTransform(offsetter)).toBe('translateX(920px)'); repeated = getRepeated(); expect(repeated.length).toBe(HORIZONTAL_PX / ITEM_SIZE + VirtualRepeatController.NUM_EXTRA); expect(repeated[0].textContent.trim()).toBe('s184s 92'); }); it('should dirty-check only the swapped scope on scroll', function() { createRepeater(); scope.items = createItems(NUM_ITEMS); scope.$apply(); $$rAF.flush(); scroller[0].scrollTop = 100; scroller.triggerHandler('scroll'); var scopes = Array.prototype.map.call(getRepeated(), function(elem) { var s = angular.element(elem).scope(); spyOn(s, '$digest').and.callThrough(); return s; }); // Scroll up by one. // Expect only the last (index 15) scope to have $digested. scroller[0].scrollTop = 90; scroller.triggerHandler('scroll'); expect(scopes[15].$digest).toHaveBeenCalled(); expect(scopes[14].$digest).not.toHaveBeenCalled(); // Scroll down by two. // Expect only the first scope to have $digested. scroller[0].scrollTop = 110; scroller.triggerHandler('scroll'); expect(scopes[0].$digest).toHaveBeenCalled(); expect(scopes[1].$digest).not.toHaveBeenCalled(); }); it('should update and preserve scroll position when the watched array increases length', function() { createRepeater(); scope.items = createItems(NUM_ITEMS); scope.$apply(); $$rAF.flush(); scroller[0].scrollTop = 100; scroller.triggerHandler('scroll'); scope.items = createItems(NUM_ITEMS+1); scope.$apply(); expect(scroller[0].scrollTop).toBe(100); expect(getRepeated()[0].textContent.trim()).toBe('s14s 7'); }); it('should update and preserve scroll position when the watched array decreases length', function() { createRepeater(); scope.items = createItems(NUM_ITEMS+1); scope.$apply(); $$rAF.flush(); scroller[0].scrollTop = 100; scroller.triggerHandler('scroll'); scope.items = createItems(NUM_ITEMS); scope.$apply(); expect(scroller[0].scrollTop).toBe(100); expect(getRepeated()[0].textContent.trim()).toBe('s14s 7'); }); it('should update and alter scroll position when the watched array decreases length (the remaining items do not fill the rest of the container)', function() { createRepeater(); scope.items = createItems(NUM_ITEMS+1); scope.$apply(); $$rAF.flush(); scroller[0].scrollTop = 100; scroller.triggerHandler('scroll'); scope.items = ['a', 'b', 'c']; scope.$apply(); expect(scroller[0].scrollTop).toBe(0); expect(getRepeated()[0].textContent.trim()).toBe('a 0'); }); it('should cap individual element size for the sizer in large item sets', function() { // Create a larger number of items than will fit in one maximum element size. var numItems = MAX_ELEMENT_PIXELS / ITEM_SIZE + 1; createRepeater(); scope.items = createItems(numItems); scope.$apply(); $$rAF.flush(); // Expect that the sizer as a whole is still exactly the height it should be. // We expect the offset to be close to the exact height, because on IE there are some deviations. expect(sizer[0].offsetHeight).toBeCloseTo(numItems * ITEM_SIZE, -1); // Expect that sizer only adds as many children as it needs to. var numChildren = sizer[0].childNodes.length; expect(numChildren).toBe(Math.ceil(numItems * ITEM_SIZE / MAX_ELEMENT_PIXELS)); // Expect that every child of sizer does not exceed the maximum element size. for (var i = 0; i < numChildren; i++) { expect(sizer[0].childNodes[i].offsetHeight).toBeLessThan(MAX_ELEMENT_PIXELS + 1); } }); it('should clear scroller if large set of items is filtered to much smaller set', function() { // Create a larger number of items than will fit in one maximum element size. var numItems = MAX_ELEMENT_PIXELS / ITEM_SIZE + 1; createRepeater(); scope.items = createItems(numItems); scope.$apply(); $$rAF.flush(); // Expect that the sizer as a whole is still exactly the height it should be. // We expect the offset to be close to the exact height, because on IE there are some deviations. expect(sizer[0].offsetHeight).toBeCloseTo(numItems * ITEM_SIZE, -1); // Expect the sizer to have children, because the the children are necessary to not exceed the maximum // size of a DOM element. expect(sizer[0].children.length).not.toBe(0); // Now that the sizer is really big, change the the number of items to be very small. numItems = 2; scope.items = createItems(numItems); scope.$apply(); $$rAF.flush(); // Expect that the sizer as a whole is still exactly the height it should be. expect(sizer[0].offsetHeight).toBe(numItems * ITEM_SIZE); // Expect that the sizer has no children, as all of items fit comfortably in a single element. expect(sizer[0].children.length).toBe(0); }); it('should start at the given scroll position', function() { scope.startIndex = 10; scope.items = createItems(200); createRepeater(); scope.$apply(); $$rAF.flush(); expect(scroller[0].scrollTop).toBe(10 * ITEM_SIZE); }); it('should shrink the container when the number of items goes down (vertical)', function() { container.attr('md-auto-shrink', ''); createRepeater(); scope.items = createItems(NUM_ITEMS); scope.$apply(); $$rAF.flush(); expect(container[0].offsetHeight).toBe(100); expect(offsetter.children().length).toBe(13); // With 5 items... scope.items = createItems(5); scope.$apply(); expect(container[0].offsetHeight).toBe(5 * ITEM_SIZE); expect(offsetter.children().length).toBe(5); // With 0 items... scope.items = []; scope.$apply(); expect(container[0].offsetHeight).toBe(0); expect(offsetter.children().length).toBe(0); // With lots of items again... scope.items = createItems(NUM_ITEMS); scope.$apply(); expect(container[0].offsetHeight).toBe(100); expect(offsetter.children().length).toBe(13); }); it('should shrink the container when the number of items goes down (horizontal)', function() { container.attr({ 'md-auto-shrink': '', 'md-orient-horizontal': '' }); createRepeater(); scope.items = createItems(NUM_ITEMS); scope.$apply(); $$rAF.flush(); expect(container[0].offsetWidth).toBe(150); expect(offsetter.children().length).toBe(18); // With 5 items... scope.items = createItems(5); scope.$apply(); expect(container[0].offsetWidth).toBe(5 * ITEM_SIZE); expect(offsetter.children().length).toBe(5); // With 0 items... scope.items = []; scope.$apply(); expect(container[0].offsetWidth).toBe(0); expect(offsetter.children().length).toBe(0); // With lots of items again... scope.items = createItems(NUM_ITEMS); scope.$apply(); expect(container[0].offsetWidth).toBe(150); expect(offsetter.children().length).toBe(18); }); it('should not shrink below the specified md-auto-shrink-min (vertical)', function() { container.attr({ 'md-auto-shrink': '', 'md-auto-shrink-min': '2' }); createRepeater(); scope.items = createItems(NUM_ITEMS); scope.$apply(); $$rAF.flush(); expect(container[0].offsetHeight).toBe(100); // With 5 items... scope.items = createItems(5); scope.$apply(); expect(container[0].offsetHeight).toBe(5 * ITEM_SIZE); // With 0 items... scope.items = []; scope.$apply(); expect(container[0].offsetHeight).toBe(2 * ITEM_SIZE); }); it('should not shrink below the specified md-auto-shrink-min (horizontal)', function() { container.attr({ 'md-auto-shrink': '', 'md-auto-shrink-min': '2', 'md-orient-horizontal': '' }); createRepeater(); scope.items = createItems(NUM_ITEMS); scope.$apply(); $$rAF.flush(); expect(container[0].offsetWidth).toBe(150); // With 5 items... scope.items = createItems(5); scope.$apply(); expect(container[0].offsetWidth).toBe(5 * ITEM_SIZE); // With 0 items... scope.items = []; scope.$apply(); expect(container[0].offsetWidth).toBe(2 * ITEM_SIZE); }); it('should measure item size after data has loaded (no md-item-size)', function() { repeater.removeAttr('md-item-size'); createRepeater(); scope.$apply(); $$rAF.flush(); expect(getRepeated().length).toBe(0); scope.items = createItems(NUM_ITEMS); scope.$apply(); $$rAF.flush(); var numItemRenderers = VERTICAL_PX / ITEM_SIZE + VirtualRepeatController.NUM_EXTRA; expect(getRepeated().length).toBe(numItemRenderers); }); it('should resize the scroller correctly when item length changes (vertical)', function() { scope.items = createItems(200); createRepeater(); scope.$apply(); $$rAF.flush(); expect(sizer[0].offsetHeight).toBe(200 * ITEM_SIZE); // Scroll down half way scroller[0].scrollTop = 100 * ITEM_SIZE; scroller.triggerHandler('scroll'); scope.$apply(); $$rAF.flush(); // Remove some items scope.items = createItems(20); scope.$apply(); $$rAF.flush(); expect(scroller[0].scrollTop).toBe(100); expect(sizer[0].offsetHeight).toBe(20 * ITEM_SIZE); // Scroll down half way scroller[0].scrollTop = 10 * ITEM_SIZE; scroller.triggerHandler('scroll'); scope.$apply(); $$rAF.flush(); // Add more items scope.items = createItems(250); scope.$apply(); $$rAF.flush(); expect(scroller[0].scrollTop).toBe(100); expect(sizer[0].offsetHeight).toBe(250 * ITEM_SIZE); }); it('should resize the scroller correctly when item length changes (horizontal)', function() { container.attr({'md-orient-horizontal': ''}); scope.items = createItems(200); createRepeater(); scope.$apply(); $$rAF.flush(); expect(sizer[0].offsetWidth).toBe(200 * ITEM_SIZE); // Scroll right half way scroller[0].scrollLeft = 100 * ITEM_SIZE; scroller.triggerHandler('scroll'); scope.$apply(); $$rAF.flush(); // Remove some items scope.items = createItems(20); scope.$apply(); $$rAF.flush(); expect(scroller[0].scrollLeft).toBe(50); expect(sizer[0].offsetWidth).toBe(20 * ITEM_SIZE); // Scroll right half way scroller[0].scrollLeft = 10 * ITEM_SIZE; scroller.triggerHandler('scroll'); scope.$apply(); $$rAF.flush(); // Add more items scope.items = createItems(250); scope.$apply(); $$rAF.flush(); expect(sizer[0].offsetWidth).toBe(250 * ITEM_SIZE); }); it('should update topIndex when scrolling', function() { container.attr({'md-top-index': 'topIndex'}); scope.items = createItems(NUM_ITEMS); createRepeater(); scope.$apply(); expect(scope.topIndex).toBe(0); scroller[0].scrollTop = ITEM_SIZE * 50; scroller.triggerHandler('scroll'); scope.$apply(); expect(scope.topIndex).toBe(50); scroller[0].scrollTop = 25 * ITEM_SIZE; scroller.triggerHandler('scroll'); scope.$apply(); expect(scope.topIndex).toBe(25); }); it('should start at the given topIndex position', function() { container.attr({'md-top-index': 'topIndex'}); repeater.removeAttr('md-start-index'); scope.topIndex = 10; scope.items = createItems(200); createRepeater(); scope.$apply(); expect(scroller[0].scrollTop).toBe(10 * ITEM_SIZE); }); it('should scroll when topIndex is updated', function() { container.attr({'md-top-index': 'topIndex'}); scope.items = createItems(200); createRepeater(); scope.topIndex = 50; scope.$apply(); expect(scroller[0].scrollTop).toBe(50 * ITEM_SIZE); scope.topIndex = 25; scope.$apply(); expect(scroller[0].scrollTop).toBe(25 * ITEM_SIZE); }); it('should recheck container size on window resize', function() { spyOn($mdUtil, 'debounce').and.callFake(angular.identity); scope.items = createItems(100); createRepeater(); // Expect 13 children (10 + 3 extra). expect(offsetter.children().length).toBe(13); container.css('height', '400px'); angular.element($window).triggerHandler('resize'); // Expect 43 children (40 + 3 extra). expect(offsetter.children().length).toBe(43); }); it('should recheck container size and scroll position on $md-resize scope ' + 'event', function() { scope.items = createItems(100); createRepeater(); // Expect 13 children (10 + 3 extra). expect(offsetter.children().length).toBe(13); container.css('height', '300px'); scope.$parent.$broadcast('$md-resize'); // Expect 33 children (30 + 3 extra). expect(offsetter.children().length).toBe(33); container.css('height', '400px'); scroller[0].scrollTop = 20; scope.$parent.$broadcast('$md-resize'); // Expect 45 children (40 + 5 extra). expect(offsetter.children().length).toBe(45); }); it('should shrink when initial results require shrinking', inject(function() { scope.items = [ { value: 'alabama', display: 'Alabama' }, { value: 'alaska', display: 'Alaska' }, { value: 'arizona', display: 'Arizona' } ]; createRepeater(); var controller = component.controller('mdVirtualRepeatContainer'); controller.autoShrink = true; controller.autoShrink_(50); expect(component[0].clientHeight).toBe(50); expect(offsetter.children().length).toBe(3); })); it('should not scroll past the bottom', function() { scope.items = createItems(101); createRepeater(); scroller[0].scrollTop = ITEM_SIZE * 91; scroller.triggerHandler('scroll'); expect(getTransform(offsetter)).toBe('translateY(880px)'); scroller[0].scrollTop++; scroller.triggerHandler('scroll'); expect(getTransform(offsetter)).toBe('translateY(880px)'); }); it('should re-render the list when switching to a smaller array', function() { scope.items = createItems(50, 'list one'); createRepeater(); scroller[0].scrollTop = 5; scroller.triggerHandler('scroll'); expect(offsetter.children().eq(0).text()).toContain('list one'); scope.$apply(function() { scope.items = createItems(25, 'list two'); }); expect(offsetter.children().eq(0).text()).toContain('list two'); }); describe('md-on-demand', function() { it('should validate an empty md-on-demand attribute value correctly', inject(function() { repeater.attr('md-on-demand', ''); createRepeater(); var containerCtrl = component.controller('mdVirtualRepeatContainer'); expect(containerCtrl.repeater.onDemand).toBe(true); })); it('should validate md-on-demand attribute with `true` correctly', inject(function() { repeater.attr('md-on-demand', 'true'); createRepeater(); var containerCtrl = component.controller('mdVirtualRepeatContainer'); expect(containerCtrl.repeater.onDemand).toBe(true); })); it('should validate md-on-demand attribute with `false` correctly', inject(function() { repeater.attr('md-on-demand', 'false'); createRepeater(); var containerCtrl = component.controller('mdVirtualRepeatContainer'); expect(containerCtrl.repeater.onDemand).toBe(false); })); }); describe('when container scope is destroyed', function() { it('should clean up unused blocks', function() { createRepeater(); var containerCtrl = component.controller('mdVirtualRepeatContainer'); scope.items = createItems(NUM_ITEMS); scope.$apply(); scope.items = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']; scope.$apply(); scope.$destroy(); var dataCount = 0; angular.forEach(containerCtrl.repeater.pooledBlocks, function(block) { dataCount += Object.keys(block.element.data()).length; }); expect(dataCount).toBe(0); }); }); /** * Facade to access transform properly even when jQuery is used; * since jQuery's css function is obtaining the computed style (not wanted) */ function getTransform(target) { return target[0].style.webkitTransform || target.css('transform'); } });